TypeScript MIT

cloudflare-workers-nextjs-saas-template

Cloudflare Workers/Next.js SaaS Template

L

LubomirGeorgiev

Dernière activité 29 sept. 2026
LubomirGeorgiev/cloudflare-workers-nextjs-saas-template

785

étoiles

169

forks

1

issues ouvertes

cloudflarecloudflare-workersnextjssaas-boilerplate

Ce README est souvent en anglais.

Cloudflare Workers SaaS Template

.github/workflows/deploy.yml

This is a SaaS template for Cloudflare Workers. It uses Vinext on Vite to run a Next.js App Router application directly on Cloudflare Workers.

What is Vinext?

Vinext is Cloudflare's experimental implementation of the public Next.js API surface on top of Vite. The goal is to let a Next.js app keep familiar App Router patterns, React Server Components, route handlers, and next/* imports while using Vite as the build and dev toolchain instead of the standard Next.js compiler pipeline.

For this template, Vinext is the runtime and deployment path for Cloudflare Workers. pnpm dev starts the Vinext development server, pnpm build produces the Vinext/Vite production output, and pnpm deploy uses vinext-cloudflare deploy to build and deploy the Worker. Vinext has first-class Cloudflare Workers support, including access to bindings such as D1, KV, R2, Images, Durable Objects, and AI through cloudflare:workers.

Vinext is not a fork of Next.js and is not affiliated with Vercel. It is still experimental, so framework-sensitive changes should be verified with pnpm run check:vinext, pnpm run typecheck, pnpm run lint, and pnpm run build.

Tip

This template is brought to you by 👉 Aski.Chat 👈 - AI customer support agents that answer visitors, capture leads, and surface customer intelligence from every conversation.

  • Website-trained AI agents for support and sales
  • Lead capture and qualification from real conversations
  • Learn what customers keep asking, then improve your docs, FAQs, and product

Turn every visitor question into a faster answer and a sharper product signal.

Supported Features:

  • 🔐 Authentication with Lucia Auth
    • 📧 Email/Password Sign In
    • 📝 Email/Password Sign Up
    • 🔑 WebAuthn/Passkey Authentication
    • 🌐 Google OAuth/SSO Integration
    • 🔄 Forgot Password Flow
    • 🔒 Change Password
    • ✉️ Email Verification
    • 🗝️ Session Management with Cloudflare KV
    • 🤖 Turnstile Captcha Integration
    • ⚡ Rate Limiting for Auth Endpoints
    • 🛡️ Protected Routes and Layouts
    • 📋 Session Listing and Management
  • 💾 Database with Drizzle and Cloudflare D1
    • 🏗️ Type-safe Database Operations
    • 🔄 Automatic Migration Generation
    • 💻 SQLite for Local Development
    • ⚡ Efficient Data Fetching
    • 🔍 Type-safe Queries
  • 📨 Email Service with Cloudflare Email Service
    • 🎨 Beautiful Email Templates
    • 👀 Email Preview Mode
    • 🔧 Local Email Development Server
    • 📬 Transactional Emails
    • ✉️ Email Verification Flow
    • 📱 Responsive Email Templates
  • 🚀 Deployment with Github Actions
    • ⚙️ Automatic Deployments
    • 🔐 Environment Variables Management
    • 📦 Database Migrations
    • 🔄 Comprehensive CI/CD Pipeline
    • 🧹 Cache Purging
    • ✅ Type Checking
    • 🧪 Integration Tests
    • 🧪 End-to-end Tests
    • 📏 Deploy Size Tracking
  • 🎨 Modern UI
    • 🎨 Tailwind CSS
    • 🧩 Shadcn UI Components
    • 🌓 Dark/Light Mode
    • 📱 Responsive Design
    • ⚡ Loading States and Animations
    • 🔔 Toast Notifications
    • ⚙️ Settings Dashboard
    • 🏠 Landing Page
    • ✨ Beautiful Email Templates
    • 👤 Profile Settings Page
    • 🎯 Form Validation States
  • 💳 Team Subscription Billing
    • 🧩 Per-team plans (Free / Pro / Enterprise) defined in code
    • 💳 Embedded Stripe Elements (Payment Element) for checkout — no redirect to hosted pages
    • 🔔 Webhook-driven subscription lifecycle (Stripe is the source of truth)
    • 🔁 In-app plan changes and cancellation
    • 🧾 Stripe Customer Portal for payment methods, invoices, and billing details
    • 🔒 Plan-based entitlements and feature gating (e.g. seat limits)
    • 🛠️ One-command Stripe setup (pnpm stripe:setup)
  • 🤖 AI-agent platform (public API, OAuth, MCP)
    • 🌐 Versioned public REST API built with Hono, reusing the app's own service layer
    • 📘 OpenAPI 3.1 document generated from the same Valibot schemas the server validates with
    • 📖 Server-rendered API reference with client-side search — no spec-viewer bundle — plus authentication and error-code guides
    • 🔑 API keys for users and teams, with scopes, optional expiry, and one-time secret display
    • 🪪 OAuth 2.1 authorization server (PKCE, dynamic client registration, consent screen)
    • 🔌 Remote MCP server whose tools are derived from the OpenAPI document — no hand-written tool list
    • 🧭 Connect guides for Claude Code, claude.ai, ChatGPT, Cursor, VS Code, Codex, Antigravity, and Grok CLI
    • 🧹 Self-service revocation from Settings → API & MCP, plus an admin view of registered OAuth apps
  • 👑 Admin Dashboard
    • 👥 User Management
    • 🪪 OAuth app registry with verification and revocation
  • 📝 Content Management System
    • 🗂️ Config-driven collections for blog and docs content
    • ✍️ Rich TipTap editor with markdown paste, markdown copy, tables, code highlighting, and alert blocks
    • 🧭 Docs navigation builder with managed public URLs
    • 🖼️ Media library with R2-backed image uploads, alt text editing, and featured images
    • 🏷️ Tags and categories with entry usage tracking
    • 🕒 Draft, published, archived, and scheduled entry workflows
    • 🧾 Version history for CMS entries
    • ⚡ KV-backed CMS entry caching and cache maintenance actions
    • 🔍 Full-text docs search
    • 🤖 AI-assisted SEO description generation
    • 🧱 Blog, docs, sitemap.xml, and JSON-LD schema rendering
    • 📄 Markdown views for public pages through the .md URL suffix, or Accept: text/markdown on the page URL
    • 🤖 llms.txt index for the API specification, MCP endpoint, and docs tree
  • ✨ Validations with Valibot and React Hook Form
    • 🛡️ Type-safe Form Validations
    • 🔒 Server-side Validations
    • 🔍 Client-side Validations
    • 🧹 Input Sanitization
    • ⚡ Real-time Validation
    • 🔄 Form State Management
  • 👨‍💻 Developer Experience
    • 🧪 Local Development Setup
    • 📘 TypeScript Support
    • 🔍 Oxlint Configuration
    • 🧪 Co-located Vitest Unit Tests
    • 🧪 Cloudflare Workers Vitest Integration Tests
    • 🧪 Vitest and Playwright E2E Tests
    • ⚡ Vinext and Vite Build Pipeline
    • 🔐 Type-safe Environment Variables
    • 🏗️ Cloudflare Types Generation
    • 🤖 AI-powered Development with AI Agents
    • 📚 Comprehensive Documentation
    • 📐 Project Structure Best Practices
  • ⚡ Edge Computing
    • 🌍 Global Deployment with Cloudflare Workers
    • 🚀 Zero Cold Starts
    • 💨 Edge Caching
    • ⚛️ React Server Components
    • 🖥️ Server-side Rendering
    • 💾 Edge Database with D1
    • 🗄️ Session Storage with KV
    • 🖼️ Cloudflare Images-powered Image Optimization
    • ⚡ API Rate Limiting
  • 🏢 Multi-tenancy Support
    • 👥 Organization Management
    • 👤 User Roles and Permissions
    • 🔍 Tenant Isolation
    • 🔄 Resource Sharing Controls
    • 📊 Per-tenant Analytics
    • 🔐 Tenant-specific Configurations
    • 💼 Team Collaboration Features
  • 🌐 Internationalization (i18n) on use-intl and an in-repo routing layer
    • 🔗 Localized URLs: the default locale is served bare, every other locale is prefixed
    • 🍪 Locale from the URL prefix, then the cookie, then Accept-Language
    • 🔀 Locale switcher in the footer
    • 🗂️ JSON message catalogs (English and Spanish included)
    • 🔒 Type-safe message keys

Internationalization (i18n)

The template owns its i18n layer. use-intl supplies the ICU translator and the React hooks; everything around it — the locale routing, the middleware, and the server API — lives in src/i18n/.

Every page lives under src/app/[locale]/, so the URL carries the locale. Prefixing is "as needed": the default locale is served at the bare path (/blog) and every other locale is prefixed (/es/blog). src/proxy.ts resolves the locale once per request — URL prefix, then the selected_locale cookie, then Accept-Language, then the default — and rewrites or redirects from that one answer. It only reads the cookie: only an explicit choice writes it, and src/i18n/locale-cookie.ts names the writers.

Module map:

Module What it owns
config.ts Locale list, default locale, served set, cookie name, switcher labels
resolve-locale.ts The one answer to "what locale is this request"
middleware.ts The pure locale route (decideLocaleRoute); src/proxy.ts is its adapter
localized-pathname.ts The one answer to "which URL serves this path in this locale"
navigation.ts Link, usePathname, useRouter, getPathname, redirect, permanentRedirect
server.ts getLocale and getTranslations for the current App Router request
client.ts / provider.tsx useTranslations/useLocale/useFormatter, and the provider above them
translator.ts getTranslator, the locale-explicit translator that needs no request scope
locale.ts / locale-actions.ts The user's own locale, and the action that stores it
locale-cookie.client.ts enterLocale: writes the locale cookie, then loads the page under the new locale
post-auth-target.ts Where a sign-in lands, and which locale cookie it writes
new-account-locale.ts The locale a new account stores: the locale of the sign-up page
messages/<locale>.json One message catalog per locale

Which import to reach for:

  • Client components: useTranslations from @/i18n/client.
  • Server components, metadata, and server actions: getTranslations from @/i18n/server.
  • Shared src/lib/** and src/utils/**, the API, and MCP: getTranslator from @/i18n/translator. Those run outside the App Router, where headers() throws.
  • Links and redirects: @/i18n/navigation, never next/navigation.

To add a locale (e.g. French):

  1. Add "fr" to LOCALES and a fr entry to LOCALE_LABELS and LOCALE_OG_MAP in src/i18n/config.ts.
  2. Add an fr loader to CATALOG_LOADERS in src/i18n/message-catalogs.ts.
  3. Create src/i18n/messages/fr.json with every key in en.json; nothing merges the default catalog in at runtime.

messages/en.json is the source of truth for type-safe keys, through the augmentation in src/i18n/use-intl.d.ts.

Planned features (TODO):

  • Update Meta SEO tags 🔍
  • Dynamic OpenGraph images 📸
  • Notifications 🔔
  • Webhooks 🔗

Running it locally

  1. pnpm install
  2. pnpx wrangler login - Login to your Cloudflare account to use Cloudflare bindings while testing locally.
  3. Copy .env.example to .env and fill in the values.
  4. pnpm db:migrate:dev - Creates a local SQLite database and applies migrations
  5. pnpm db:seed - Seeds the database with test data
  6. pnpm dev - Starts the Vinext development server
  7. Go to http://localhost:3000/sign-in and login with the test user credentials: test@test.com / password
  8. Go to http://localhost:3000/admin to manage users and the CMS.

Billing setup (team subscriptions)

Billing is per-team: each team owns a single Stripe subscription to a plan (Free / Pro / Enterprise). Plans are defined in code in src/constants/plans.json and consumed by both the app and the setup script, so Stripe and code never drift. Payment collection uses embedded Stripe Elements (the Payment Element), and plan changes/cancellation are handled by in-app server actions. The hosted Stripe Customer Portal complements this for self-service payment method updates, invoice history, and billing details — the billing page shows a "Manage billing" button once the team has a Stripe customer.

The subscription lifecycle is webhook-driven: Stripe is the source of truth and the DB is a cache updated from webhook events. The app never mutates subscription state from the client's confirmPayment result alone.

Billing is enabled only when STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, and NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY are all set (see isBillingEnabled() in src/flags.ts); otherwise the billing UI is hidden and the webhook route no-ops.

Happy path:

  1. Create a Stripe account and copy sk_test_... / pk_test_... into .env (STRIPE_SECRET_KEY, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY).
  2. Run pnpm stripe:setup — this idempotently creates the products + recurring prices for the paid plans and, by default, upserts the resolved STRIPE_PRICE_PRO / STRIPE_PRICE_ENTERPRISE values into .env (replacing existing keys in place, never duplicating). When yearly billing is enabled (see below) it also creates the yearly prices and writes STRIPE_PRICE_PRO_YEAR / STRIPE_PRICE_ENTERPRISE_YEAR. It refuses to run against a live key unless you pass --live. Use --dry-run to preview without changing anything, or --no-write to create the Stripe resources but only print the values instead of touching .env.
  3. Local webhooks: stripe listen --forward-to localhost:3000/api/stripe/webhook, then copy the printed whsec_... into .env as STRIPE_WEBHOOK_SECRET.
  4. Production: pnpm stripe:setup --with-webhook https://<domain>/api/stripe/webhook, then set the secrets via wrangler secret put STRIPE_SECRET_KEY and wrangler secret put STRIPE_WEBHOOK_SECRET.
  5. Manual Dashboard alternative: create the Products + recurring Prices yourself, add a webhook endpoint for the subscription/invoice events to /api/stripe/webhook, and copy the price IDs + signing secret into your env.
  6. Customer Portal: pnpm stripe:setup also provisions a portal configuration (written to .env as STRIPE_PORTAL_CONFIG_ID) that enables payment method updates, invoice history, and billing-details editing — including business name, address, and VAT/tax IDs, which Stripe prints on invoices automatically. Plan changes and cancellation are deliberately disabled in the portal because they're handled by in-app server actions; keeping a single code path avoids conflicting subscription state. If STRIPE_PORTAL_CONFIG_ID is unset, sessions fall back to the account's default portal configuration (Settings → Billing → Customer portal).

The billing page lives at /dashboard/teams/[teamSlug]/billing (the generic /dashboard/billing nav item redirects there for the selected team).

Yearly billing: set yearlyDiscountPercent in src/constants/plans.json (e.g. 20) to offer every monthly paid plan as a yearly subscription at that percentage off — the yearly amount is derived as monthly × 12 × (100 − discount) / 100, the billing page gains a Monthly/Yearly toggle with a "Save X%" badge, and existing subscribers can switch interval in place (prorated by Stripe). Re-run pnpm stripe:setup after changing it so the yearly prices exist. Remove the key to keep billing monthly-only.

Entitlements & downgrades: feature access is derived from the team's plan via getTeamEntitlements (src/utils/entitlements.ts). Plan limits (e.g. seats) are enforced only at grow points such as inviting members — a team that drops to a lower plan keeps its existing members and is never auto-evicted.

AI-agent platform (public API, OAuth, MCP)

The template ships a public machine surface next to the web app: a versioned REST API, an OpenAPI document, an OAuth 2.1 authorization server, and a remote MCP server. They are one pipeline rather than four features — an endpoint is described once and shows up in the reference, in openapi.json, and as an agent tool.

Public REST API. A Hono app mounted at /api/v1 (src/api/). Handlers call the same src/lib/** service functions the app's server actions use, so business rules exist in exactly one place. Requests are authenticated by a bearer credential, rate limited per credential by the app's shared KV limiter (RATE_LIMITS.API_AUTHED / RATE_LIMITS.API_ANON in src/utils/with-rate-limit.ts), and failures come back as RFC 9457 problem documents whose code member is a stable, untranslated identifier.

OpenAPI + docs. GET /api/v1/openapi.json is generated from the Valibot request/response schemas in src/schemas/api/ and the describeRoute metadata on each route. The reference at /docs/api is our own: every operation, field, example, and curl snippet is rendered on the server from that document, and the only client JavaScript is an instant filter over the already-rendered endpoints. /docs/authentication, /docs/api/errors, and /docs/mcp cover credentials, error codes, and agent setup. Add .md to a public page URL to get Markdown, or send Accept: text/markdown to the page URL and the Worker redirects there. /llms.txt points agents at the spec, MCP endpoint, and docs tree. How a fork extends the API and MCP surface is repo documentation, not a public page: docs/extending-api-and-mcp.md.

API keys. Users create keys under Settings → API & MCP, teams under team settings (gated by the MANAGE_API_KEYS team permission). A key carries scopes and an optional expiry, its secret is displayed once, and only a hash plus the last characters are stored. Keys are cached in KV for a few minutes with the same version/TTL discipline as sessions, so revocation is immediate locally and propagates in about a minute. The prefixes live at the top of src/constants.ts — rebrand them when forking, because secret scanners attribute a leaked key to whoever owns the prefix.

OAuth 2.1 provider. @cloudflare/workers-oauth-provider wraps the Worker in worker-entrypoint.ts. It serves the discovery documents, the token endpoint, Client ID Metadata Document identity, and (when OAUTH_OPEN_DCR_ENABLED is on) dynamic client registration; the consent screen at /oauth/authorize is our own page. Stable CIMD or operator-issued client identities are preferred for trusted integrations. DCR remains the compatibility fallback: every generated client ID is kept distinct, shown as unverified, and clamped to DCR_ALLOWED_SCOPES until an admin verifies that exact registration at /admin/oauth-apps. Users see and revoke every connected app under Settings → API & MCP, alongside the agent-connection guide and their API keys.

MCP server. /mcp speaks Streamable HTTP and accepts both credential types. Tools are derived from the OpenAPI document at request time (src/mcp/derive-tools.ts) and filtered by the caller's scopes, so tools/list never advertises something the credential could not call. Tool calls dispatch in-process into the Hono app — no self-fetch.

Connecting an agent. Hosted assistants (claude.ai, ChatGPT) connect over OAuth: paste the MCP URL and approve the consent screen. CLI and editor clients (Claude Code, Codex, Cursor, VS Code, Antigravity, Grok CLI) can do that too, or send an API key as an Authorization: Bearer header. Ready-made snippets for each client are rendered from src/constants/agent-clients.ts on the API-keys settings page and at /docs/mcp.

Extending it in your fork:

File What to change there
src/api/index.ts registerCustomRoutes — mount your routers; they inherit auth, rate limiting, and problem+json errors
src/schemas/api/ Valibot request/response schemas (these also type the derived agent tools)
src/lib/api/scopes.ts The scope catalog and the ceiling for self-registered OAuth clients
src/mcp/tool-overrides.ts Rename, re-describe, or hide a derived tool by operationId
src/mcp/index.ts registerCustomTools — agent tools with no REST equivalent
src/constants/agent-clients.ts The agent-client registry behind every setup snippet

No extra secrets are needed: the provider generates and stores its own key material. The Cloudflare-side additions are covered under Changes to wrangler.jsonc.

End-to-end tests

E2E tests use Vitest with Playwright-driven Chromium pages and run against the production-style local Worker preview.

After cloning the project, install the Playwright Chromium browser once on your machine:

pnpm exec playwright install chromium

This browser binary is not downloaded automatically by pnpm install. It is kept outside the repo in Playwright's local browser cache, so you normally only need to run the install command after a fresh machine setup, a new Playwright version, or a cleared Playwright cache.

Run the E2E suite with:

pnpm run test:e2e

The E2E runner stores its temporary files under tmp/e2e, creates a fresh local Wrangler/D1 state under tmp/e2e/wrangler-state, applies all D1 migrations, runs src/db/seed.sql, builds the app, starts Wrangler preview on that isolated state, then runs the browser tests against the preview Worker. If the existing dist output matches the current build input fingerprint, the runner reuses that fresh Vinext build instead of rebuilding. The build and D1 setup run in parallel, and Vitest runs test files in parallel with isolated Playwright browser contexts. This keeps E2E data separate from your normal local .wrangler development state.

VS Code Vitest Explorer is configured through .vscode/settings.json to use vitest.e2e.config.ts. Running an individual E2E test from the editor uses Vitest global setup to create the same isolated Wrangler/D1 state and preview Worker before the selected test starts.

In CI, install Chromium before running the suite:

pnpm exec playwright install --with-deps chromium
pnpm run test:e2e

Useful commands

Command Purpose
pnpm dev Start the Vinext development server
pnpm build Build the app with Vinext and Vite
pnpm start Start the local Vinext production server
pnpm preview Build, then preview the Worker locally with Wrangler
pnpm run test:unit Run co-located Vitest unit tests
pnpm run test:integration Run Cloudflare Workers Vitest integration tests with local D1/KV/Queue bindings
pnpm run test:e2e Run Playwright-driven E2E tests against a clean local Wrangler/D1 preview
pnpm deploy Build and deploy with vinext-cloudflare deploy
pnpm deploy:dryrun Build and run a Wrangler deploy dry run into worker-dist
pnpm check:vinext Run the Vinext compatibility checker
pnpm run lint Run Oxlint
pnpm run typecheck Run TypeScript without emitting files
pnpm run cf-typegen Regenerate Cloudflare Worker types

Changes to wrangler.jsonc

After making a change to wrangler.jsonc, run pnpm run cf-typegen to regenerate worker-configuration.d.ts.

Cloudflare bindings are defined in wrangler.jsonc and exposed to server code through cloudflare:workers or the local helper in src/utils/cloudflare-context.ts. The custom Worker entry lives in worker-entrypoint.ts and is configured as the main entry in wrangler.jsonc.

Bindings worth knowing about when you fork:

  • D1_DB, KV_STORE, and R2_BUCKET — the app database, the shared KV namespace, and the media bucket. They were named NEXT_TAG_CACHE_D1, NEXT_INC_CACHE_KV, and NEXT_INC_CACHE_R2_BUCKET before; rename them in your own wrangler.jsonc when you merge this change. The resource ids do not change, so no data moves.
  • OAUTH_KV — a second binding onto the same KV namespace as KV_STORE, not a new namespace. @cloudflare/workers-oauth-provider hardcodes the binding name, and its keys are prefix-scoped (see OAUTH_RESERVED_KV_PREFIXES in src/constants.ts), so nothing collides with the app's own keys. Point it at your own namespace id together with KV_STORE.
  • The global_fetch_strictly_public compatibility flag is required by the OAuth provider's client-metadata fetches; leave it in place.

Things to change and customize before deploying to production

  1. Go to src/constants.ts and update it with your project details — including API_KEY_PREFIX_LIVE / API_KEY_PREFIX_TEST, so leaked keys are attributed to you and not to the template
  2. Update the name field in package.json to your project name so generated metrics and package metadata identify the reused template correctly
  3. Update AGENTS.md with your project specification so that AI coding agents can give you better suggestions
  4. Update the footer in src/components/footer.tsx with your project details and links
  5. Replace the logo — the template ships a placeholder mark (three cascading planes in the marketing amber). It appears in the header, the footer lockup, every generated OpenGraph card, and the favicon. src/constants/logo.ts is the only source: swap the two gradient stops to rebrand it in place, or edit LOGO_PLANE_PATHS to replace the artwork. Then run pnpm logo:generate to rewrite the three static copies, src/app/icon.svg, src/app/favicon.ico, and public/logo.svg. pnpm run test:unit fails while a copy is stale
  6. Update the OpenGraph card colors in the COLORS map at the top of src/lib/og/og-image.tsx. These are literal hex mirrors of the dark-theme tokens in globals.css, duplicated because satori resolves neither CSS variables nor oklch() — so restyling the app does not restyle the social cards, and they will keep shipping the template's amber until you change them here too
  7. Optional: Update the color palette in src/app/globals.css
  8. Update the metadata in src/utils/root-metadata.ts (authors, creator, the Twitter handle) with your project details — the root layout, src/app/[locale]/layout.tsx, reads it
  9. Update cms.config.ts if necessary

Production deployment

The source of truth for preparing and deploying this template to production is the repo-local AI agent skill at .agents/skills/prepare-cloudflare-production-deployment/SKILL.md.

That skill covers Cloudflare resource provisioning with Cloudflare MCP, GitHub Actions secrets and variables with the GitHub CLI, wrangler.jsonc binding updates, Worker secrets, Turnstile, Email Sending, and deployment verification.

Preflight for the team_security_hardening migration (existing databases only)

This migration promotes previously non-unique indexes to unique ones. On a database with existing team data, run these read-only checks first — any returned rows must be deduplicated manually (keep the active/oldest row, repoint references) before applying the migration, or CREATE UNIQUE INDEX will fail mid-deploy:

-- Duplicate team memberships (same team + user more than once)
SELECT teamId, userId, COUNT(*) AS n FROM team_membership GROUP BY teamId, userId HAVING n > 1;

-- Duplicate role names within a team
SELECT teamId, name, COUNT(*) AS n FROM team_role GROUP BY teamId, name HAVING n > 1;

Run them with wrangler d1 execute <DB_NAME> --remote --command "<query>".

The migration also deletes all pending (unaccepted) team invitations: invitation tokens are now stored hashed, so pre-existing plaintext-token invite links can no longer be redeemed. After deploying, ask team owners to resend any outstanding invitations.

Projets similaires

Open Next.js adapter for Cloudflare

TypeScriptcloudflarenextjsreact
Oopennextjs
1,9 k étoiles150

Templates for Cloudflare Workers

TypeScript
Ccloudflare
2,1 k étoiles1 k

🔥 Enterprise SaaS Starter Kit - Kickstart your enterprise app development with the Next.js SaaS boilerplate 🚀

TypeScriptboilerplateboilerplateshacktoberfest
Bboxyhq
4,9 k étoiles1,2 k