| name | cloudflare-workers |
| description | Cloudflare Workers and Durable Objects conventions for TypeScript projects. Covers wrangler.jsonc configuration, type-safe env via `wrangler types` and `import { env } from 'cloudflare:workers'`, secrets.required for typed secrets, custom_domain for routing, preview/production environments, deploy scripts, Durable Objects with SQLite, and Spiceflow as the web framework with Vite. ALWAYS load this skill when a project uses wrangler, Cloudflare Workers, Durable Objects, or deploys to Cloudflare. Load it before writing any wrangler config, worker code, or deploy scripts.
|
Cloudflare Workers
Conventions for Cloudflare Workers and Durable Objects in TypeScript projects.
Framework: Spiceflow with Vite + @cloudflare/vite-plugin
Always use Spiceflow as the web framework for Workers. Load the spiceflow skill first — it has the full API reference and conventions.
import { cloudflare } from '@cloudflare/vite-plugin'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
import spiceflow from 'spiceflow/vite'
export default defineConfig({
plugins: [
react(),
spiceflow({ entry: './src/app.tsx' }),
cloudflare({
viteEnvironment: {
name: 'rsc',
childEnvironments: ['ssr'],
},
}),
],
})
Entry file is always src/app.tsx — uses JSX for .page() routes. The entry file also exports the Cloudflare Worker default fetch handler and any DO class re-exports. No separate worker.ts file — the app.tsx IS the worker entry.
{
"main": "./src/app.tsx"
}
import { Spiceflow } from 'spiceflow'
import { env } from 'cloudflare:workers'
export { MyStore } from './my-store.ts'
export const app = new Spiceflow()
.page('/', async () => <h1>Home</h1>)
export default {
async fetch(request: Request): Promise<Response> {
return app.handle(request)
},
} satisfies ExportedHandler<Env>
Background tasks with waitUntil
All background promises (fire-and-forget work like analytics, logging, cache writes, webhook processing) MUST use waitUntil. Never do void somePromise() or somePromise().catch(...) directly; the Workers runtime kills the isolate as soon as the response is sent, so untracked promises are silently dropped.
Inside a spiceflow route or middleware, use waitUntil from the handler context:
export const app = new Spiceflow().route({
method: 'POST',
path: '/webhook',
async handler({ request, waitUntil }) {
const payload = await request.json()
waitUntil(processWebhookInBackground(payload))
return { ok: true }
},
})
Outside a route (e.g. inside a Durable Object, a utility function, or the top-level fetch handler), import waitUntil from cloudflare:workers:
import { waitUntil } from 'cloudflare:workers'
async function doSomething() {
waitUntil(trackEvent('something_happened'))
}
Configuration: wrangler.jsonc
Always use wrangler.jsonc (not wrangler.toml). Newer features are exclusive to the JSON format.
compatibility_date: ALWAYS use today's date
MUST: Always set compatibility_date to today's date minus 30 days (we can't use today's date directly because the wrangler version used should also released after that day or it will show an error) when creating a new worker or updating an existing one. Old dates disable newer runtime features like WeakRef, FinalizationRegistry, and other JS globals — causing cryptic "X is not defined" errors at runtime. There is no benefit to using an old date unless you are pinning behavior for a production worker you cannot test.
{
"compatibility_date": "2026-04-14",
}
Type-safe environment
Generate types with wrangler types
wrangler types generates a worker-configuration.d.ts file with a typed Env interface derived from your wrangler.jsonc bindings. This replaces @cloudflare/workers-types entirely.
"types": "wrangler types"
After generating types:
- Uninstall
@cloudflare/workers-types — it conflicts with generated runtime types
- Install
@types/node if using nodejs_compat
- Include
worker-configuration.d.ts in tsconfig:
{
"compilerOptions": {
"types": []
},
"include": ["src", "worker-configuration.d.ts"]
}
- Rerun
wrangler types every time you change wrangler.jsonc
NEVER define custom Env types
The generated worker-configuration.d.ts declares a global Env interface. Never create your own Env type or interface. All bindings, vars, and secrets are available on Env automatically.
export interface Env {
MY_KV: KVNamespace
API_KEY: string
}
export class MyDO extends DurableObject<Env> { ... }
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) { ... }
} satisfies ExportedHandler<Env>
Importing common types
The generated types include all Cloudflare runtime types. Import only from cloudflare:workers for Worker-specific classes:
import { DurableObject } from 'cloudflare:workers'
import { env } from 'cloudflare:workers'
All other types are available globally from the generated file — DurableObjectState, DurableObjectStorage, KVNamespace, ExecutionContext, ExportedHandler, DurableObjectNamespace, DurableObjectStub, etc. No imports needed.
import { env } from 'cloudflare:workers'
function getStub() {
const id = env.MY_STORE.idFromName('main')
return env.MY_STORE.get(id)
}
export default {
async fetch(request: Request): Promise<Response> {
const stub = getStub()
return stub.handleRequest(request)
},
} satisfies ExportedHandler<Env>
Avoid overriding fetch() on Durable Objects
Prefer named RPC methods over overriding fetch() on DOs. RPC methods are type-safe, self-documenting, and avoid the legacy fetch-based routing pattern.
export class MyStore extends DurableObject<Env> {
async handleRequest(request: Request): Promise<Response> { ... }
async hranaHandler(request: Request): Promise<Response> { ... }
async restore(timestamp: number) { ... }
}
export class MyStore extends DurableObject<Env> {
async fetch(request: Request): Promise<Response> { ... }
}
The worker calls stub.handleRequest(request) or stub.hranaHandler(request) directly — clear what each method does, and TypeScript checks the call.
Fixing DurableObjectNamespace generics
wrangler types generates DurableObjectNamespace without the generic type param, so the stub type is DurableObjectStub<undefined> — RPC methods are invisible. Interface augmentation doesn't work because the existing property type wins in the intersection.
Fix with a typed helper that casts the stub return:
import { env } from 'cloudflare:workers'
import type { MyStore } from './my-store.ts'
export function getStub() {
const id = env.MY_STORE.idFromName('main')
return env.MY_STORE.get(id) as DurableObjectStub<MyStore>
}
Import and call getStub() instead of accessing env.MY_STORE directly.
Secrets
Declare secrets in wrangler.jsonc
Use secrets.required to declare secrets. This makes wrangler types generate typed string properties on Env, and wrangler deploy validates they are set.
{
"secrets": {
"required": ["API_KEY", "DB_PASSWORD", "AUTH_SECRET"]
}
}
After adding secrets, rerun wrangler types. The generated Env will include:
interface Env {
API_KEY: string;
DB_PASSWORD: string;
AUTH_SECRET: string;
}
Local development: use Doppler, not .env
Do not use checked-in .env files for Worker local development in this workspace. Use Doppler to inject local env vars and secrets into wrangler dev / vite dev instead.
Wrangler local dev now loads local dev vars from .env files or the process environment, so doppler run works fine for local Worker runtime bindings. Keep secrets.required in wrangler.jsonc so local dev only loads the keys the Worker actually expects.
doppler run -c development -- wrangler dev
CLOUDFLARE_ENV=preview doppler run -c preview -- vite dev
CLOUDFLARE_ENV=preview doppler run -c preview -- vite build && wrangler deploy --env preview
Rules:
- Prefer Doppler over
.env / .dev.vars for local development.
- Put shell env vars before
doppler run, never after.
- Read runtime values from
import { env } from 'cloudflare:workers', not process.env, even though process.env may be populated under nodejs_compat.
Upload secrets from Doppler to Cloudflare
Cloudflare Workers store their own deployed secret values. Local doppler run is only for local development — it does not upload secrets to Cloudflare. Sync them explicitly with wrangler secret bulk.
{
"scripts": {
"secrets:preview": "doppler run -c preview --mount .env.preview --mount-format env -- wrangler secret bulk --env preview .env.preview",
"secrets:prod": "doppler run -c production --mount .env.prod --mount-format env -- wrangler secret bulk .env.prod"
}
}
Run these whenever Worker secrets change:
pnpm secrets:preview
pnpm secrets:prod
Do not loop over wrangler secret put one key at a time. It is interactive and hangs in scripts. Always use wrangler secret bulk.
Production / preview secret values
wrangler secret put API_KEY
wrangler secret put API_KEY --env preview
Prefer the bulk upload scripts above over manual secret put commands.
KV operations: always use --remote
wrangler kv commands default to local storage, not the deployed remote KV. If you kv key list, kv key get, or kv key put without --remote, you're reading/writing to a local SQLite file that the deployed worker never sees. This causes confusing debugging sessions where writes appear to succeed but data seems missing.
wrangler kv key list --namespace-id abc123
wrangler kv key get --namespace-id abc123 "my-key"
wrangler kv key put --namespace-id abc123 "my-key" "value"
wrangler kv key list --namespace-id abc123 --remote
wrangler kv key get --namespace-id abc123 "my-key" --remote
wrangler kv key put --namespace-id abc123 "my-key" "value" --remote
When debugging whether a Worker's KV writes are persisting, always use --remote on the verification commands. The Worker itself always writes to the remote KV; only the wrangler CLI defaults to local.
KV consistency model
KV.get() is strongly consistent in the datacenter that wrote the key. Cross-datacenter reads are eventually consistent (up to 60s).
KV.list() is always eventually consistent, even in the same datacenter. Recently written keys may not appear for several seconds.
- Use
KV.getWithMetadata(key) (checking value !== null) instead of KV.list() when verifying that specific keys exist after writing them.
Dynamic workers with LOADER
See ./dynamic-workers.md
Importing non-JS files as text
For things like .txt, .md, and .sql, tell Wrangler/Vite to import them as text with rules, then add a TypeScript declaration file. Do not silence the import with // @ts-expect-error.
{
"rules": [
{ "type": "Text", "globs": ["**/*.sql"], "fallthrough": true },
{ "type": "Text", "globs": ["**/*.md", "**/*.txt"], "fallthrough": true }
]
}
declare module '*.sql' {
const content: string
export default content
}
declare module '*.md' {
const content: string
export default content
}
declare module '*.txt' {
const content: string
export default content
}
import schemaSql from './schema.sql'
import promptMd from './prompt.md'
import fixtureTxt from './fixture.txt'
Use a real declare module file so TypeScript understands the import shape. Never paper over missing module types with @ts-expect-error.
Routing: prefer custom_domain when you actually need routing
Do not add routes / custom_domain entries just because a project uses Spiceflow, Vite, or @cloudflare/vite-plugin. Spiceflow does not need wrangler routing rules to run, build, or deploy, and Vite does not need them either.
Only add routes when you are intentionally binding a real hostname to the worker. If you do need that, prefer custom_domain instead of path-based routes. Custom domains work without needing a proxied A/AAAA DNS record first — Cloudflare creates it automatically.
{
"routes": [
{ "pattern": "api.example.com", "custom_domain": true },
{ "pattern": "api.preview.example.com", "custom_domain": true, "zone_name": "example.com" }
]
}
Use routes (non-custom_domain) only when you need path-based routing (example.com/api/*) on a domain that already has another worker or Pages project on the root.
If you are using the default *.workers.dev hostname, or you have not decided on a custom domain yet, leave routes out entirely.
Environments: preview and production
Every project has two environments. Preview is the default for development and testing.
wrangler.jsonc structure
Critical: bindings are NOT inherited by environments. Wrangler environments do not inherit durable_objects, kv_namespaces, secrets, r2_buckets, etc. from the top level. You MUST duplicate all bindings in both top-level (production) and env.preview. If you don't, wrangler types generates optional (?) types for bindings that only exist in one environment, causing possibly undefined errors everywhere.
Only vars values need to differ between environments. Everything else (bindings, secrets, migrations) should be identical.
{
"name": "my-worker",
"compatibility_date": "2026-04-14",
"compatibility_flags": ["nodejs_compat"],
"main": "./src/app.tsx",
"durable_objects": {
"bindings": [{ "name": "MY_STORE", "class_name": "MyStore" }]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["MyStore"] }
],
"vars": {
"APP_URL": "https://app.example.com"
},
"secrets": {
"required": ["API_KEY", "AUTH_SECRET"]
},
"routes": [
{ "pattern": "app.example.com", "custom_domain": true }
],
"env": {
"preview": {
"name": "my-worker-preview",
"durable_objects": {
"bindings": [{ "name": "MY_STORE", "class_name": "MyStore" }]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["MyStore"] }
],
"vars": {
"APP_URL": "https://app.preview.example.com"
},
"secrets": {
"required": ["API_KEY", "AUTH_SECRET"]
},
"routes": [
{ "pattern": "app.preview.example.com", "custom_domain": true, "zone_name": "example.com" }
]
}
}
}
Deploy scripts
The @cloudflare/vite-plugin resolves and flattens your wrangler.jsonc at build time and writes it into dist/rsc/wrangler.json. Set CLOUDFLARE_ENV during vite build so the plugin resolves the correct environment section:
wrangler deploy deploys one environment at a time. It does not deploy every configured env.* block. With no --env flag, Wrangler deploys the top-level/default config (usually production). Use wrangler deploy --env preview or another explicit env name when targeting a non-production environment.
IMPORTANT: Cloudflare D1 does NOT auto-apply migrations on deploy. If you deploy new worker code that references columns or tables from a pending migration, the worker will crash with "no such table" or "no such column" errors. Always run D1 migrations before deploying. Bake them into the deploy scripts so they can't be skipped.
Always deploy preview first, then production. D1 migrations can fail (bad SQL, constraint violations on existing data) and there is no automatic rollback. Running against preview first catches these failures safely. If the preview migration or deploy fails, stop. Do not continue to production.
For projects using D1, bake migrations into the deploy chain. The remote migration scripts print a unix timestamp before running so you can restore via D1 time travel if something goes wrong:
{
"scripts": {
"deploy": "pnpm db:migrate:preview && CLOUDFLARE_ENV=preview vite build && wrangler deploy --env preview",
"deploy:prod": "pnpm db:migrate:prod && vite build && wrangler deploy"
}
}
If a migration corrupts data, use the printed timestamp to restore:
wrangler d1 time-travel restore DB --timestamp=<unix_timestamp>
wrangler d1 time-travel restore DB --timestamp=<unix_timestamp> --env preview
For projects without D1 (no migrations needed):
{
"scripts": {
"deploy": "CLOUDFLARE_ENV=preview vite build && wrangler deploy --env preview",
"deploy:prod": "vite build && wrangler deploy"
}
}
pnpm deploy → migrates + builds for preview env, deploys to preview (safe default)
pnpm deploy:prod → migrates + builds for production, deploys to production
Preview is the default deploy target. This prevents accidental production deploys. Production deploys should be deliberate.
Deployment sequence for D1 projects:
pnpm deploy
pnpm deploy:prod
Secrets per environment
Secrets are set per environment. Set them separately:
wrangler secret put API_KEY --env preview
wrangler secret put API_KEY
Using preview for integration tests
Preview environments are useful for tests that depend on Cloudflare infrastructure (Durable Objects, KV, R2, etc.) which can't be fully emulated locally.
import { describe, test, expect } from 'vitest'
const PREVIEW_URL = 'https://app.preview.example.com'
describe('integration', () => {
test('health check', async () => {
const res = await fetch(`${PREVIEW_URL}/health`)
expect(res.status).toBe(200)
const body = await res.json()
expect(body).toEqual({ ok: true })
})
test('auth flow redirects to provider', async () => {
const res = await fetch(`${PREVIEW_URL}/api/auth/sign-in/social?provider=sigillo`, {
redirect: 'manual',
})
expect(res.status).toBe(302)
expect(res.headers.get('location')).toContain('auth.sigillo.dev')
})
})
Deploy to preview first, then run tests against it:
pnpm deploy && pnpm vitest --run test/integration.test.ts
Testing with Vitest inside workerd
Tests run inside the real workerd runtime via @cloudflare/vitest-pool-workers. This means env, waitUntil, D1, KV, R2, Durable Objects — all Cloudflare APIs work in tests without mocks. Miniflare simulates every binding locally as in-memory state; no real Cloudflare infrastructure is needed.
vite.config.ts setup
The key pattern: swap between cloudflareTest() (tests) and cloudflare() (dev/build) based on process.env.VITEST. Both can live in the same vite.config.ts.
import path from 'node:path'
import { cloudflare } from '@cloudflare/vite-plugin'
import { cloudflareTest, readD1Migrations } from '@cloudflare/vitest-pool-workers'
import spiceflow from 'spiceflow/vite'
import { defineConfig } from 'vite'
export default defineConfig(async () => {
const migrations = await readD1Migrations(path.join(__dirname, 'migrations')).catch(() => [])
return {
plugins: [
process.env.VITEST
? cloudflareTest({
wrangler: { configPath: './wrangler.jsonc' },
miniflare: {
bindings: { TEST_MIGRATIONS: migrations },
},
})
: cloudflare({
viteEnvironment: { name: 'rsc', childEnvironments: ['ssr'] },
}),
spiceflow({ entry: './src/main.tsx' }),
],
test: {
setupFiles: ['./src/apply-migrations.ts'],
},
}
})
If you have no D1, omit readD1Migrations and the miniflare.bindings option entirely.
Applying D1 migrations before tests
Create a setup file that runs inside workerd before each test file:
import { applyD1Migrations } from 'cloudflare:test'
import { env } from 'cloudflare:workers'
await applyD1Migrations(env.DB, env.TEST_MIGRATIONS)
Add TEST_MIGRATIONS to your type declarations so env.TEST_MIGRATIONS is typed:
declare namespace Cloudflare {
interface Env {
TEST_MIGRATIONS: D1Migration[]
}
}
interface D1Migration {
name: string
queries: string[]
}
Storage isolation model
All storage (D1, KV, R2, Durable Objects) follows the same isolation model:
- Per test file — each file gets a fresh storage snapshot; writes are invisible to other files
- Shared within a file — tests within the same file see each other's writes
- Automatic reset — workerd uses an on-disk SQLite snapshot stack: "pushes" a fresh snapshot at file start, "pops" it at file end, discarding all writes
This means setup files like apply-migrations.ts run once per test file, applying migrations to a fresh in-memory DB each time.
Durable Objects follow the same per-file isolation. DO instances created in one file don't exist in another. listDurableObjectIds(namespace) only returns IDs created within the current file's storage context.
pnpm test
│
├─ users.test.ts ├─ posts.test.ts
│ Fresh D1 + fresh DO storage Fresh D1 + fresh DO storage
│ ├─ setup: apply migrations ├─ setup: apply migrations
│ ├─ test 1 writes D1/DO ├─ test 1 writes D1/DO
│ └─ test 2 sees test 1's state └─ test 2 sees test 1's state
│ (file ends → all state discarded) (file ends → all state discarded)
│
│ Files run concurrently. Each sees only its own storage.
If you need per-test isolation within a file: clean up manually in beforeEach/afterEach (e.g. DELETE FROM table or env.KV.delete(key)).
If you need shared state across files (e.g. integration tests with accumulated data): run with --max-workers=1 --no-isolate.
WebSockets + Durable Objects don't work with per-file isolation. Use --max-workers=1 --no-isolate as a workaround.
Key test APIs
From cloudflare:workers:
| Import | Purpose |
|---|
env | All bindings from wrangler.jsonc — typed via Cloudflare.Env |
waitUntil | Register background promises (same as ctx.waitUntil) |
exports | Access exports.default.fetch() to hit the Worker handler directly |
From cloudflare:test:
| Import | Purpose |
|---|
applyD1Migrations(db, migrations) | Apply SQL migration files to a D1 binding |
runInDurableObject(stub, callback) | Run a callback inside a DO instance — inspect state, call methods |
runDurableObjectAlarm(stub) | Immediately fire a scheduled DO alarm |
listDurableObjectIds(namespace) | List all DO IDs created in the current file's storage context |
createExecutionContext() | Create a ctx object for passing to raw worker handlers |
waitOnExecutionContext(ctx) | Wait for all ctx.waitUntil() promises to settle |
For the full reference including Queues, Workflows, and Scheduled handlers, see Cloudflare Workers Vitest test APIs.
Durable Objects with SQLite
See the drizzle skill for full schema and migration conventions. Key wrangler config:
{
"rules": [
{ "type": "Text", "globs": ["**/*.sql"], "fallthrough": true }
],
"durable_objects": {
"bindings": [
{ "name": "MY_STORE", "class_name": "MyStore" }
]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["MyStore"] }
]
}
The rules entry is required for drizzle DO migrations — imports .sql files as text.
Usage counter pattern (exact billing / rate limiting)
For exact per-entity counters (API call tracking, usage-based billing, hard rate limits), use a Durable Object with SQLite storage. Each entity (project, user, org) gets its own DO instance via idFromName(), so counters are isolated and increments are atomic SQL statements with no read-modify-write races.
Full implementation: copy ./usage-counter-do.ts (bundled with this skill) into your project. No external dependencies.
Usage is stored as append-only event rows with timestamps, so you can query any time window (current billing month, last 7 days, all time). Totals are derived by summing rows.
import { env } from 'cloudflare:workers'
import type { UsageCounter } from './usage-counter-do.ts'
function getUsageStub(projectId: string) {
const id = env.USAGE_COUNTER.idFromName(projectId)
return env.USAGE_COUNTER.get(id) as DurableObjectStub<UsageCounter>
}
await getUsageStub('proj_123').record('api-calls')
await getUsageStub('proj_123').record('tokens', 1500)
const monthStart = new Date('2026-07-01').getTime()
const apiCalls = await getUsageStub('proj_123').getTotalSince('api-calls', monthStart)
const breakdown = await getUsageStub('proj_123').getBreakdownSince(monthStart)
const threeMonthsAgo = Date.now() - 90 * 24 * 60 * 60 * 1000
await getUsageStub('proj_123').pruneOlderThan(threeMonthsAgo)
The DO hibernates after 10s of inactivity, so each write only costs the few ms of execution time. At $0.15/million requests and negligible duration, this is much cheaper than KV ($5/million writes) and fully atomic unlike KV's eventually-consistent read-modify-write.
For approximate usage tracking (dashboards, analytics), use Analytics Engine instead. It handles sampling and high cardinality but doesn't give exact counts.
Memoizing slow operations with the Cache API
Workers run globally on 300+ datacenters, but your database (D1, Postgres, etc.) lives in one region. Cross-region reads can be 50-200ms. Use the Cache API (caches.default) to memoize slow lookups at the edge so repeated reads from the same datacenter are ~1-5ms.
Each datacenter has its own independent cache. No cross-datacenter replication. First request to a datacenter is always a miss, subsequent requests are fast. This is ideal for data that changes rarely (auth checks, config, org membership, environment lookups).
The memoize() utility wraps any async function. Args are superjson-serialized and SHA-256 hashed into cache keys. Supports stale-while-revalidate: within the SWR window, stale values return immediately while a background refresh runs via waitUntil(). Cache keys include the spiceflow deployment id so stale entries from old builds are never served.
Requires a custom domain. Does NOT work on *.workers.dev.
Full implementation: copy ./worker-memoize.ts (bundled with this skill) into your project as lib/memoize.ts. Dependencies: superjson, cloudflare:workers, spiceflow.
Usage example — memoize auth and config lookups:
import { memoize } from './lib/memoize.ts'
const lookupOrgMember = memoize({
namespace: 'org-member',
fn: async (userId: string, orgId: string) => {
const db = getDb()
const member = await db.query.orgMember.findFirst({ where: { userId, orgId } })
if (!member) return null
return { role: member.role }
},
})
const getOrgIdForProject = memoize({
namespace: 'project-org',
fn: async (projectId: string) => {
const db = getDb()
const row = await db.query.project.findFirst({
where: { id: projectId },
columns: { orgId: true },
})
return row?.orgId ?? null
},
})
null, undefined, and Error results are never cached. This prevents caching "not found" or "unauthorized" responses that would lock users out until the TTL expires. Memoized functions that indicate absence or failure MUST return null/undefined or throw. If a background SWR refresh returns null/Error, the stale cache entry is evicted so the next request hits the database.
What to memoize vs skip:
| Memoize | Skip |
|---|
| Auth/membership checks | Session validation (BetterAuth has its own cookieCache) |
| Org/project ownership lookups | Secrets (change frequently, stale = security risk) |
| OAuth client id by hostname | Encryption keys (CPU, not I/O) |
| Environment resolution (id/slug) | Write operations |
package.json scripts
Standard scripts for a Worker package (with D1):
{
"scripts": {
"dev": "pnpm db:migrate:local && vite dev",
"build": "vite build",
"typecheck": "tsc",
"types": "wrangler types",
"db:migrate:local": "wrangler d1 migrations apply DB --local",
"db:migrate:prod": "echo \"D1 pre-migration timestamp: $(date +%s)\" && wrangler d1 migrations apply DB --remote",
"db:migrate:preview": "echo \"D1 pre-migration timestamp: $(date +%s)\" && wrangler d1 migrations apply DB --remote --env preview",
"deploy": "pnpm db:migrate:preview && CLOUDFLARE_ENV=preview vite build && wrangler deploy --env preview",
"deploy:prod": "pnpm db:migrate:prod && vite build && wrangler deploy"
}
}
Standard scripts for a Worker package (without D1):
{
"scripts": {
"dev": "vite dev",
"build": "vite build",
"typecheck": "tsc",
"types": "wrangler types",
"deploy": "CLOUDFLARE_ENV=preview vite build && wrangler deploy --env preview",
"deploy:prod": "vite build && wrangler deploy"
}
}