# Deploy to Cloudflare (/en/docs/deployment/cloudflare)

Deploy the **TanStack Start** app as one Cloudflare Worker with hosted Supabase.
Start with the [TanStack Start app working locally](/en/docs/quickstart). The
Next.js template deploys through [Vercel](/en/docs/deployment/vercel).

## Before you deploy

Use your application's Cloudflare account, hosted Supabase project and test-mode
billing configuration. Install the [Cloudflare CLI](https://github.com/cloudflare/cf)
and confirm your selected account:

```bash
npm i -g cf
cf --version
cf auth whoami
```

The build output is compatible with `cf` **1.0.0-beta.14**. If you are logged out,
run `cf auth login`, then check the account again. Stop if it is not the account
that should own your app. Check [Worker size limits](https://developers.cloudflare.com/workers/platform/limits/#worker-size):
the included app is larger than the 3 MiB compressed free-plan limit. Do not
assume that a successful local build fits your plan.

<Steps>
  <Step>
    ### Name the Worker and choose its origin

    In `vite.config.ts`, change `nitro.cloudflare.wrangler.name` from `app` to
    your application's Worker name. The Cloudflare configuration is inside the
    existing `nitro(...)` call. Keep the pinned `compatibilityDate`; changing it
    changes Worker runtime behavior.

    Record the intended HTTPS origin,
    `https://<worker-name>.<your-subdomain>.workers.dev`. `<your-subdomain>` is
    your account's workers.dev subdomain, shown on the **Workers & Pages**
    overview in the Cloudflare dashboard. Use this origin for browser
    configuration, Auth redirects and billing return URLs. A custom domain can
    be configured later through Cloudflare.
  </Step>

  <Step>
    ### Link Supabase and apply migrations

    In the [Supabase dashboard](https://supabase.com/dashboard), create or
    select the hosted project for your application. Confirm its project name
    and reference before linking this checkout:

    ```bash interactive=true
    bunx supabase login
    bunx supabase link --project-ref <your-project-ref>
    bunx supabase migration list --linked
    bunx supabase db push --linked --dry-run
    ```

    The dry run lists pending migrations without applying them. For a new
    project, review that list, apply the template migrations, then check the
    migration history again:

    ```bash
    bunx supabase db push --linked
    bunx supabase migration list --linked
    ```

    All checked-in migration versions must appear in the remote history. For
    an existing or shared project, stop on an unexpected migration or schema
    mismatch and reconcile it before deploying. Do not reset an existing database.
  </Step>

  <Step>
    ### Allow Auth redirects

    Under **Authentication → URL Configuration**, add these exact Redirect URLs,
    replacing the example origin with your Worker origin:

    ```text interactive=true
    https://<worker-name>.<your-subdomain>.workers.dev/auth/callback
    https://<worker-name>.<your-subdomain>.workers.dev/auth/reset-password
    https://<worker-name>.<your-subdomain>.workers.dev/dashboard/account?status=Success!&status_description=Your%20email%20has%20been%20updated.
    ```

    * The callback receives signup, email and OAuth returns.
    * The reset URL receives password setup and recovery links.
    * The account URL receives email-change confirmation, including the query
      string the app generates.

    If the Worker is the project's only app, set **Site URL** to its HTTPS origin.
    When adding Workers to an existing shared project, preserve its Site URL and
    all existing redirect entries, and add the Worker URLs alongside them. Keep
    the configured provider callback in Supabase's OAuth settings unchanged. Test
    each enabled account flow at the Worker URL before advertising it.
  </Step>

  <Step>
    ### Create the Hyperdrive configuration

    Create a Hyperdrive configuration for this database by following
    [Cloudflare's Supabase guide](https://developers.cloudflare.com/hyperdrive/examples/connect-to-postgres/postgres-database-providers/supabase/),
    or reuse an existing configuration that points to that exact database.
    Hyperdrive is a connection proxy; it does not create another database.

    * Use the Supabase direct connection as its origin.
    * Disable query caching so permissions and application reads use current
      database state.
    * Keep origin TLS enabled. If the database uses a private CA, configure its
      [trusted CA certificate](https://developers.cloudflare.com/hyperdrive/configuration/tls-ssl-certificates-for-hyperdrive/)
      rather than disabling TLS.

    List your configurations and record the ID of the one for this database:

    ```bash
    cf hyperdrive list
    ```

    Set that ID as `CLOUDFLARE_HYPERDRIVE_ID` in the deployment file below. The
    build binds it as `HYPERDRIVE`; the Worker uses its connection string with
    request-scoped clients and prepared statements.
  </Step>

  <Step>
    ### Optional: register billing webhooks

    Hosted billing and production email on Workers are
    [not yet verified](/en/docs/deployment#tanstack-start-deployment-status). You
    can deploy without them and add them later. If you configure billing now,
    stay in Stripe test mode or Polar sandbox and register a new hosted webhook
    destination for your provider at your Worker origin:

    ```text interactive=true
    https://<worker-name>.<your-subdomain>.workers.dev/api/webhooks/stripe
    https://<worker-name>.<your-subdomain>.workers.dev/api/webhooks/polar
    ```

    Use the signing secret of that hosted destination, never your local
    listener's secret.
  </Step>

  <Step>
    ### Prepare deployment settings

    Create an ignored deployment file from the example:

    ```bash
    cp .env.example .env.cloudflare
    ```

    Edit `.env.cloudflare` locally with your provider values. Replace all
    loopback URLs and placeholders. Collect the Supabase values from the
    dashboard first:

    * Under **Connect**, copy the hosted PostgreSQL connection for
      `DATABASE_URL`; Supabase's
      [connection guide](https://supabase.com/docs/guides/database/connecting-to-postgres)
      helps you choose a connection type. The Worker itself connects through the
      Hyperdrive binding created above.
    * Under **Project Settings → API Keys**, copy the publishable and server-only
      secret keys, and note the project's API URL.

    At minimum, configure:

    | Setting                                    | Value for this deployment                                                        |
    | ------------------------------------------ | -------------------------------------------------------------------------------- |
    | `NEXT_PUBLIC_APP_URL`                      | The exact HTTPS Worker origin.                                                   |
    | `NEXT_PUBLIC_SUPABASE_URL`                 | Your hosted Supabase API URL.                                                    |
    | `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`     | The matching public key.                                                         |
    | `DATABASE_URL`                             | Your hosted Postgres connection for database tooling and Node. Server-only.      |
    | `CLOUDFLARE_HYPERDRIVE_ID`                 | Existing Hyperdrive configuration ID for this same database; used at build time. |
    | `SUPABASE_SECRET_KEY`                      | The matching server-only Supabase key.                                           |
    | `EMAIL_DELIVERY_MODE`                      | `real`, set before any flow sends email.                                         |
    | `PAYMENT_PROVIDER` and the provider's keys | Optional for a first deploy; see the list below.                                 |

    Also set, for the same file:

    * **Email:** `RESEND_API_KEY` and `RESEND_FROM_EMAIL`. A production build
      rejects an unset `EMAIL_DELIVERY_MODE` when it sends email.
    * **Stripe:** `PAYMENT_PROVIDER=stripe` with `STRIPE_SECRET_KEY`,
      `STRIPE_WEBHOOK_SECRET`, `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` and
      `NEXT_PUBLIC_STRIPE_MODE`.
    * **Polar:** `PAYMENT_PROVIDER=polar` with the `POLAR_*` settings in
      `.env.example`.

    `.env.example` leaves `SUPABASE_SECRET_KEY` commented out; uncomment it.
    The deploy step uploads every setting in this file as a Worker secret, so
    replace each `replace_me` placeholder you keep. Before you sell, also set
    the product and price IDs for your offers: the commented
    `NEXT_PUBLIC_STRIPE_*_PRICE_ID*` lines for Stripe, or the `POLAR_*_PRODUCT_ID*`
    lines for Polar.

    Only `NEXT_PUBLIC_*` settings are exposed to browser code. Database,
    administrative, payment and email secrets remain server-side. Do not put
    them in `vite.config.ts`, commit the deployment file, or paste its values
    into a shell command or support message.
  </Step>

  <Step>
    ### Build and check the artifact

    ```bash
    bun --env-file=.env.cloudflare run build:cloudflare
    cf deploy --prebuilt --dry-run
    ```

    The build uses Nitro's Workers preset, checks TypeScript, then prepares
    the server modules and public assets under `.cloudflare/output/` for the
    Cloudflare CLI. It does not read `.env.local`. The dry run validates the
    artifact without uploading it. Both commands must finish successfully.

    Public values are fixed into the browser bundle during this build.
    Rebuild whenever the public origin or other `NEXT_PUBLIC_*` values change.
  </Step>

  <Step>
    ### Deploy the built app

    ```bash
    cf deploy --prebuilt --secrets-file .env.cloudflare
    ```

    The CLI uploads the prepared app and stores the supplied runtime settings
    as Worker secrets. It prints the deployed URL and version. Confirm the
    Worker name and URL match your intended app before testing it.
  </Step>
</Steps>

## Verify the hosted app

Open the printed URL, then verify the parts of your product that you configured.
To sign in, first create an account at `/signup` with the email address of a
member of your Supabase project team. Supabase's default email service sends
confirmation email only to those addresses until you
[configure custom SMTP](https://supabase.com/docs/guides/auth/auth-smtp).

| Check                                     | Expected result                                                              |
| ----------------------------------------- | ---------------------------------------------------------------------------- |
| Home, pricing, Docs and Blog              | Pages and assets load on a direct visit and refresh.                         |
| `/api/health`                             | HTTP 200 with `status: ok`. This checks liveness only.                       |
| Sign in, reload, sign out                 | The session survives reload and protected pages require signin after logout. |
| Create a Project                          | The Project remains after refresh in the same Organization.                  |
| Your selected billing flow, if configured | Test checkout returns to the Worker origin and payment grants access.        |
| Your application's social image           | The emitted image URL returns an image rather than an error page.            |

A successful deployment or health response does not prove authentication,
database writes, payment handling or email delivery. Stop on a failed check;
inspect the affected Worker request in Cloudflare logs before changing provider
credentials or retrying a payment.

## Update or recover

Rebuild and rerun the dry run before deploying a change. Keep the previous
Cloudflare version until the new one passes your checks; use Cloudflare's
[rollback procedure](https://developers.cloudflare.com/workers/configuration/versions-and-deployments/rollbacks/)
if a deployed change fails. Do not reset Supabase to repair a Worker deployment.

## What's next

<Cards>
  <Card title="Authentication" href="/en/docs/authentication" description="Configure and verify your application's account flows." />

  <Card title="Get Support" href="/en/docs/support" description="Share the Worker URL and the sanitized error if a check fails." />
</Cards>
