Skip to content

Instantly share code, notes, and snippets.

@slawton3
Created April 16, 2025 03:28
Show Gist options
  • Save slawton3/509f61c8e764e544d063cdd93b53c363 to your computer and use it in GitHub Desktop.
Save slawton3/509f61c8e764e544d063cdd93b53c363 to your computer and use it in GitHub Desktop.
Deploying TanStack Start Apps to Cloudflare Workers with GitHub Actions

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:

/**
* 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:

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.

# 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.

  1. 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.

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:

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



DATABASE_URL="your_local_or_dev_db_connection_string"

# ... add all other secrets/variables needed locally ...
  1. 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.

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)
# 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment