Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save kziemski/074c3bb5743322fe8a7fae933bd3e6c8 to your computer and use it in GitHub Desktop.

Select an option

Save kziemski/074c3bb5743322fe8a7fae933bd3e6c8 to your computer and use it in GitHub Desktop.

Revisions

  1. @slawton3 slawton3 created this gist Apr 16, 2025.
    487 changes: 487 additions & 0 deletions tanstack-start-cf-workers-gh.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,487 @@
    # Deploying TanStack Start Apps to Cloudflare Workers with GitHub Actions

    Maybe you're a vibe coder, or you're a seasoned vet who's frustrated with Next and server components. Either way, I'll show you how to set up continuous deployment for a TanStack Start application to Cloudflare Workers using GitHub Actions (securely, of course).

    **Assumption:**

    - You have a TanStack Start project initialized.
    - Package manager like `pnpm` or `npm`

    **Prereqs:**

    1. **Cloudflare Account:** Grab an API token with the appropriate template.

    2. **GitHub Repository:** Your TanStack Start project hosted on GitHub.

    3. **Node.js & pnpm:** Installed locally.

    4. **Wrangler CLI:** (Optional but recommended for local testing) Installed globally or as a dev dependency (`pnpm add -D wrangler`).

    5. **GitHub CLI (`gh`):** (Optional) Useful for managing secrets programmatically if you have a lot.

    ## Step 1: Configure Build for Cloudflare Workers

    TanStack Start uses Vinxi, which builds upon Nitro. Ensure your build process is configured to output a Cloudflare Workers compatible format.

    Check your app configuration (in `app.config.ts`) and ensure the Nitro preset targets Cloudflare Workers `preset: 'cloudflare-module'`.

    How do we know this is an option? It's not in the Tanstack Start docs (currently), but you can just check the guts of the Vinxi deployment presets:

    ```typescript
    /**
    * Not all the deployment presets are fully functional or tested.
    * @see https://github.com/TanStack/router/pull/2002
    */

    declare const vinxiDeploymentPresets: readonly ["alwaysdata", "aws-amplify", "aws-lambda", "azure", "azure-functions", "base-worker", "bun", "cleavr", "cli", "cloudflare", "cloudflare-module", "cloudflare-pages", "cloudflare-pages-static", ...];

    type DeploymentPreset = (typeof vinxiDeploymentPresets)[number] | (string & {});
    ```

    Now, install the `unenv` package.

    `pnpm add unenv`

    The server property in `app.config.ts` should look like this:

    ```typescript
    import { cloudflare } from "unenv";

    ...

    server: {
    preset: "cloudflare-module",
    unenv: cloudflare,
    },
    ```

    - Verify that your build script (e.g., `pnpm build`) generates the worker entry point, typically at `.output/server/index.mjs`.

    ## Step 2: Configure Cloudflare Worker (`wrangler.toml`)

    Create a `wrangler.toml` file in the root of your project. This file configures your Cloudflare Worker deployment.

    ```toml

    # wrangler.toml

    # Replace with your desired worker name (must be unique within your account)

    name = "your-app-name"

    # Entry point for your worker, generated by the build process

    main = ".output/server/index.mjs"

    # Enable Node.js compatibility APIs if your app or dependencies need them

    compatibility_flags = ["nodejs_compat"]


    # Use a recent compatibility date

    compatibility_date = "2024-09-19" # Or a later date

    # Example: Serve static assets built by Vinxi/Vite

    # The binding name "ASSETS" must match how you reference it in your server code (if needed)

    # Check Vinxi/Nitro Cloudflare preset docs for asset handling specifics.

    # assets = { directory = "./.output/public/", binding = "ASSETS" }



    # Example: Route traffic from your custom domain to this worker

    # routes = [{ pattern = "your-domain.com", custom_domain = true }]



    # Define bindings for Cloudflare resources (KV, R2, D1, etc.)

    # These names must match the variable names used in your Worker code



    [[kv_namespaces]]

    binding = "YOUR_KV_BINDING_NAME" # e.g., SESSION_KV

    id = "your_kv_namespace_id" # Find in Cloudflare Dashboard



    [[r2_buckets]]

    binding = "PROFILE_BUCKET" # e.g., USER_PROFILES_BUCKET

    bucket_name = "user-profiles" # Your R2 bucket name

    # Add other bindings (D1 databases, Queues, etc.) as needed



    # [vars] section is NOT used for secrets during deployment with this GitHub Action setup.

    # Secrets are injected securely via the workflow.

    # Use a separate `.dev.vars` file for local development secrets.



    [observability]

    # Optional: Enable observability features if needed

    enabled = true

    ```

    - **`name`**: Your Worker's name on Cloudflare.

    - **`main`**: Path to the built server entry point. Adjust if your build output differs.

    - **`compatibility_flags`**: Crucial if your code uses Node.js APIs (`fs`, `path`, `crypto`, `http`, etc.).

    - **`compatibility_date`**: Use a recent date.

    - **`assets`**: (Optional) Configure if/how static assets are served directly by the Worker. Consult Vinxi/Nitro documentation for the best approach with the Cloudflare preset.

    - **`routes`**: (Optional) Define how traffic is routed to your worker (e.g., from a custom domain).

    - **Bindings (`[[kv_namespaces]]`, `[[r2_buckets]]`, etc.)**: Define connections to other Cloudflare resources. The `binding` name is how you access the resource in your Worker code (e.g., `env.PROFILE_BUCKET`).

    ## Step 3: Set up GitHub Secrets

    **Never commit secrets directly into your code or `wrangler.toml`.** Use GitHub Actions secrets.

    1. Go to your GitHub repository > Settings > Secrets and variables > Actions.

    2. Click "New repository secret" for each secret your application needs at runtime _and_ for deployment.

    3. **Required Deployment Secrets:**

    - `CLOUDFLARE_API_TOKEN`: Generate a Cloudflare API token with "Edit Workers" permissions (My Profile > API Tokens > Create Token).

    - `CLOUDFLARE_ACCOUNT_ID`: Find this on the right sidebar of your Cloudflare dashboard's Workers & Pages overview page.

    4. **Application Secrets:** Add secrets for _all_ environment variables your deployed Worker needs (based on your `.env` file, excluding client-side `VITE_*` variables unless also needed server-side):

    - `DATABASE_URL`

    - _(Add any others specific to your application)_

    ## Step 4: Create GitHub Actions Workflow

    Create a file named `.github/workflows/deploy.yml`. This workflow automates the build and deployment process when you push to the `main` branch.

    ```yaml

    name: Deploy to Cloudflare Workers


    on:

    push:

    branches:

    - main



    jobs:

    deploy:

    runs-on: ubuntu-latest

    name: Deploy

    permissions:

    contents: read

    steps:

    - name: Checkout

    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: 20

    cache: "pnpm"



    - name: Install dependencies

    run: pnpm install --frozen-lockfile



    - name: Build application for Cloudflare Workers

    run: pnpm build

    env:

    VITE_BASE_URL: ${{ secrets.VITE_BASE_URL }}



    - name: Deploy to Cloudflare Workers

    uses: cloudflare/wrangler-action@v3

    id: deploy

    with:

    apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}

    accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

    secrets: |
    DATABASE_URL

    ... rest of secrets

    env:

    DATABASE_URL: ${{ secrets.DATABASE_URL }}
    ... rest of env vars

    ```

    **Wtf is this?:**

    - **`on: push: branches: [main]`**: Triggers the workflow on pushes to the `main` branch.

    - **`jobs: deploy: runs-on: ubuntu-latest`**: Defines a job named `deploy` running on the latest Ubuntu runner.

    - **`steps`**: The sequence of actions:

    - Checkout code.

    - Set up pnpm and Node.js (with caching).

    - Install dependencies using `pnpm install --frozen-lockfile`.

    - Build the application using `pnpm build`. (Note the optional `env` section for build-time variables).

    - Deploy using `cloudflare/wrangler-action@v3`:

    - `apiToken`, `accountId`: Provided from GitHub secrets.

    - `secrets:`: A multiline list of secret _names_ that the action should upload to Cloudflare Workers secrets.

    - `env:`: Makes the corresponding GitHub secrets available as environment variables _to the action itself_, which is necessary for the underlying `wrangler secret bulk` command.

    ## Step 5: Local Dev

    For running `wrangler dev` locally:

    1. Create a `.dev.vars` file in your project root.

    2. Add your local environment variables/secrets to this file in `KEY=VALUE` format:

    ```ini

    # .dev.vars (Add this file to .gitignore!)



    DATABASE_URL="your_local_or_dev_db_connection_string"

    # ... add all other secrets/variables needed locally ...

    ```

    3. **IMPORTANT:** Add `.dev.vars` to your `.gitignore` file.

    Wrangler will automatically load variables from `.dev.vars` when you run `wrangler dev`.

    ## Step 6: Add to `.gitignore`

    Ensure your `.gitignore` file includes:

    ```
    # .gitignore
    # Secrets and Local Overrides
    .env
    .dev.vars
    # Build output
    .output/
    dist/
    .vinxi/
    # Dependencies
    node_modules/
    # Wrangler state
    .wrangler/
    ```

    ## Step 7: Send it

    Commit `wrangler.toml`, `.github/workflows/deploy.yml`, and your updated `.gitignore`. Push your changes to the `main` branch.

    ```bash

    git add wrangler.toml .github/workflows/deploy.yml .gitignore CLOUDFLARE_DEPLOYMENT.md

    git commit -m "feat: Configure Cloudflare Workers deployment via GitHub Actions"

    git push origin main

    ```

    Check the "Actions" tab in your GitHub repo

    ---

    _(Optional: Script to Set GitHub Secrets)_

    Setting secrets is a pain in the ass. Skip the manual work with the GitHub CLI (`gh`) to automate adding secrets from your local `.env` file to your GitHub repository. Run it from the `scripts` directory.\_

    - _Create `scripts/set-github-secrets.sh` (ensure it's executable: `chmod +x scripts/set-github-secrets.sh`)_

    ```bash

    # scripts/set-github-secrets.sh

    #!/bin/bash



    # Reads secrets from ../.env and sets them in the current GitHub repo via gh cli.

    # Prerequisites: gh cli installed and authenticated (`gh auth login`).



    ENV_FILE="../.env"

    REPO_OWNER_AND_NAME="$(gh repo view --json nameWithOwner -q .nameWithOwner)"



    if [ -z "$REPO_OWNER_AND_NAME" ]; then

    echo "Error: Could not automatically determine repository name."

    exit 1

    fi



    if [ ! -f "$ENV_FILE" ]; then

    echo "Error: .env file not found at $ENV_FILE"

    exit 1

    fi



    echo "Setting secrets for repository: $REPO_OWNER_AND_NAME"



    while IFS= read -r line || [ -n "$line" ]; do

    line=$(echo "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')

    if [ -z "$line" ] || [[ "$line" =~ ^# ]]; then

    continue

    fi

    if [[ "$line" =~ ^VITE_ ]]; then

    echo "Skipping VITE_ variable: $line"

    continue

    fi

    if [[ "$line" =~ ^([^=]+)=(.*) ]]; then

    SECRET_NAME="${BASH_REMATCH[1]}"

    SECRET_VALUE="${BASH_REMATCH[2]}"

    SECRET_VALUE=$(echo "$SECRET_VALUE" | sed -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'$//")

    echo "Setting secret: $SECRET_NAME ..."

    if gh secret set "$SECRET_NAME" --repo "$REPO_OWNER_AND_NAME" -b"$SECRET_VALUE"; then

    echo "Secret $SECRET_NAME set successfully."

    else

    echo "Error setting secret $SECRET_NAME."

    fi

    else

    echo "Skipping malformed line: $line"

    fi

    done < "$ENV_FILE"



    echo "Finished setting secrets."

    ```
    - _Run `./scripts/set-github-secrets.sh`_