Add tinyship-cloudflare skill for all-in Cloudflare setup

New skill that guides users through a complete Cloudflare stack setup:
- Workers deployment (TanStack Start)
- D1 database or Hyperdrive+PostgreSQL (user chooses)
- R2 object storage
- Email Sending (REST API)
- Turnstile bot protection
- Production deployment with secrets management

Includes 6 reference docs for detailed per-service setup steps.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Viking Zhang
2026-06-16 22:07:20 +08:00
co-authored by Cursor
parent 555a42883c
commit 0261b8b6d6
9 changed files with 858 additions and 0 deletions
+1
View File
@@ -13,6 +13,7 @@ When a user asks for help with their TinyShip project, direct them to the releva
- **Deployment**: `skills/tinyship-deploy/SKILL.md` — Docker, Vercel, Cloudflare Workers, VPS
- **New Features**: `skills/tinyship-feature/SKILL.md` — monorepo feature development checklist
- **AI Integration**: `skills/tinyship-ai/SKILL.md` — chat, image gen, video gen providers
- **Cloudflare All-in**: `skills/tinyship-cloudflare/SKILL.md` — Workers + D1/Hyperdrive + R2 + Email + Turnstile
## Skill Format
+2
View File
@@ -32,6 +32,7 @@ npx skills add TinyshipCN/tinyship-skills --list
| **tinyship-feature** | New feature development — guided monorepo feature workflow with libs, config, i18n, permissions |
| **tinyship-deploy** | Deployment guide — Docker, Vercel, Cloudflare Workers, VPS/PM2 |
| **tinyship-ai** | AI feature integration — chat, image generation, video generation with credit system |
| **tinyship-cloudflare** | All-in Cloudflare setup — Workers + D1/Hyperdrive + R2 + Email Sending + Turnstile |
## Usage
@@ -42,6 +43,7 @@ After installation, just ask your AI agent naturally:
- *"Add Stripe payment"* → triggers `tinyship-payment`
- *"Deploy to Docker"* → triggers `tinyship-deploy`
- *"Add a new feature for user profiles"* → triggers `tinyship-feature`
- *"Set up Cloudflare for my project"* → triggers `tinyship-cloudflare`
## Supported Agents
+153
View File
@@ -0,0 +1,153 @@
---
name: tinyship-cloudflare
description: >-
One-stop Cloudflare setup for TinyShip: Workers + D1/Hyperdrive + R2 + Email Sending +
Turnstile. Use when the user asks to "set up cloudflare", "all-in cloudflare", "deploy to
workers", "configure d1", "configure r2", "cf setup", "cloudflare full stack", or wants
to use the Cloudflare ecosystem for their TinyShip project.
---
# TinyShip Cloudflare Setup
One-stop wizard to configure the full Cloudflare stack for TinyShip (TanStack Start).
**This is a conversational, step-by-step wizard. You MUST ask the user at each
decision point and wait for their answer before proceeding. NEVER make choices
on the user's behalf.**
> **Scope:** This skill configures TanStack Start (`apps/tanstack-app`) for Cloudflare
> Workers deployment. Next.js and Nuxt.js are designed for Node.js environments and
> do not deploy to Workers. If the user hasn't chosen TanStack Start yet, suggest
> running the `tinyship-setup` skill first.
## Prerequisites
Before starting, verify:
1. **Node.js >= 22.20.0** — run `node -v`
2. **pnpm >= 9.0.0** — run `pnpm -v`
3. **Dependencies installed** — if `node_modules/` is missing, run `pnpm install`
4. **A Cloudflare account** — https://dash.cloudflare.com/sign-up
## Step 1: Wrangler Login
Run the following to authenticate with Cloudflare:
```bash
cd apps/tanstack-app
npx wrangler login
```
This opens a browser for authorization. After success, verify:
```bash
npx wrangler whoami
```
Capture the **Account ID** from the output — it will be reused for R2 and Email.
If the user is already logged in (`wrangler whoami` shows account info), skip this step
and tell the user.
## Step 2: Database — D1 or Hyperdrive
**⛔ STOP — You MUST ask the user before proceeding.**
Present these options and wait for the user's response:
| Option | Database | Best For |
|--------|----------|----------|
| **A** | **Cloudflare D1** (SQLite) | All-in Cloudflare, zero external deps, low cost |
| **B** | **Hyperdrive + PostgreSQL** | Reuse existing PG, high write concurrency, data parity with Next/Nuxt |
Explain the trade-offs:
- **D1**: Fully managed, included in Workers billing, 5-10 GB limit, single-writer constraint
- **Hyperdrive**: Needs an external PostgreSQL, but no storage limits, same DB across all 3 apps
### If the user chooses D1:
Read and follow [references/d1-setup.md](references/d1-setup.md).
### If the user chooses Hyperdrive:
Read and follow [references/hyperdrive-setup.md](references/hyperdrive-setup.md).
**After database is configured, verify locally:**
```bash
cd apps/tanstack-app
pnpm dev:cf
```
Check that the app loads and database-dependent pages work. Tell the user the results.
## Step 3: R2 Storage (Optional)
**⛔ STOP — You MUST ask the user before proceeding.**
Ask: **Do you want to set up Cloudflare R2 for file storage (images, uploads)?**
- **Yes** — read and follow [references/r2-setup.md](references/r2-setup.md)
- **No** — skip (can be configured later)
## Step 4: Email Sending (Optional)
**⛔ STOP — You MUST ask the user before proceeding.**
Ask: **Do you want to use Cloudflare Email Sending for transactional emails
(verification codes, password resets)?**
- **Yes** — read and follow [references/email-setup.md](references/email-setup.md)
- **No** — skip (default is Resend, or configure later)
## Step 5: Turnstile Bot Protection (Optional)
**⛔ STOP — You MUST ask the user before proceeding.**
Ask: **Do you want to enable Cloudflare Turnstile (CAPTCHA) for login/signup protection?**
- **Yes** — read and follow [references/turnstile-setup.md](references/turnstile-setup.md)
- **No** — skip (disabled by default, safe to add later)
## Step 6: Deploy to Workers
**⛔ STOP — You MUST ask the user before proceeding.**
Ask: **Do you want to deploy to Cloudflare Workers now, or just finish the local setup?**
- **Deploy now** — read and follow [references/workers-deploy.md](references/workers-deploy.md)
- **Later** — tell the user they can deploy anytime with `cd apps/tanstack-app && pnpm run deploy:cf`
## Step 7: Summary
After setup is complete, provide a summary of what was configured:
```
✅ Cloudflare Setup Complete
─────────────────────────────
Workers: TanStack Start on Cloudflare Workers
Database: [D1 / Hyperdrive+PG] — configured and verified
Storage: [R2 / skipped]
Email: [Cloudflare Email / skipped]
Turnstile: [enabled / skipped]
Deployment: [deployed to <url> / ready for deploy]
Next steps:
- Run `cd apps/tanstack-app && pnpm dev:cf` for local CF dev
- Run `cd apps/tanstack-app && pnpm run deploy:cf` to deploy
- Use `npx wrangler tail` to view live logs
```
## Reference Files in TinyShip Repo
When executing this skill, read these files from the user's TinyShip project for context:
- `apps/tanstack-app/wrangler.jsonc` — current Wrangler config
- `apps/tanstack-app/CF-NOTES.md` — known Cloudflare pitfalls
- `apps/tanstack-app/.dev.vars.example` — local dev vars template
- `apps/tanstack-app/vite.config.ts` — CF plugin conditional loading
- `docs/user-guide/deployment/cloudflare-workers.md` — full deployment guide
- `config/storage.ts` — R2 config structure
- `config/email.ts` — Email config structure
- `config/captcha.ts` — Turnstile config structure
- `env.example` — all environment variables
@@ -0,0 +1,128 @@
# D1 Database Setup
## 1. Create D1 Database
```bash
cd apps/tanstack-app
npx wrangler d1 create tinyship-db
```
Capture the returned `database_id` from the output.
## 2. Configure wrangler.jsonc
Edit `apps/tanstack-app/wrangler.jsonc`:
1. **Set `DB_DIALECT` to `d1`** in the `vars` section
2. **Add the `d1_databases` binding** with the database ID from step 1
3. **Remove or comment out the `hyperdrive` block** — D1 and Hyperdrive should not be active simultaneously
```jsonc
{
"vars": {
"DB_DIALECT": "d1",
"APP_BASE_URL": "http://localhost:7001",
"BETTER_AUTH_URL": "http://localhost:7001"
},
"d1_databases": [
{
"binding": "DB",
"database_name": "tinyship-db",
"database_id": "<paste-your-database-id-here>",
"migrations_dir": "../../libs/database/drizzle-sqlite"
}
]
// Remove or comment out any "hyperdrive" block
}
```
**Important:** The `migrations_dir` must point to `../../libs/database/drizzle-sqlite` —
this is where Drizzle generates SQLite/D1 migration files.
## 3. Generate and Apply Migrations
```bash
# From the monorepo root — generate SQLite migration files
pnpm db:generate:sqlite
# From apps/tanstack-app — apply migrations to remote D1
cd apps/tanstack-app
npx wrangler d1 migrations apply tinyship-db
# For local D1 preview environment
npx wrangler d1 migrations apply tinyship-db --local
```
## 4. Seed Data (Optional)
D1 cannot use `pnpm db:seed` directly (the seed script uses `better-sqlite3` which
cannot connect to D1). Two approaches:
### Option A: Register through the app (recommended for production)
Deploy first, then register accounts through the app UI. No pre-seeding needed.
### Option B: Export from local SQLite and import to D1
```bash
# 1. From monorepo root — seed local SQLite
mkdir -p data
pnpm db:push:sqlite
pnpm db:seed:sqlite
# 2. Export INSERT statements
{
echo "PRAGMA foreign_keys=OFF;"
sqlite3 data/local.sqlite '.mode insert' '.dump user' | grep '^INSERT'
sqlite3 data/local.sqlite '.mode insert' '.dump account' | grep '^INSERT'
sqlite3 data/local.sqlite '.mode insert' '.dump blog_post' | grep '^INSERT'
} > data/seed-data.sql
# 3. Import to remote D1
cd apps/tanstack-app
npx wrangler d1 execute tinyship-db --remote --file=../../data/seed-data.sql
# 4. Verify
npx wrangler d1 execute tinyship-db --remote --command "SELECT email, name, role FROM user"
```
## 5. Configure .dev.vars
Copy the template and set required secrets for local CF development:
```bash
cd apps/tanstack-app
cp .dev.vars.example .dev.vars
```
Edit `.dev.vars`:
```
APP_BASE_URL=http://localhost:7001
BETTER_AUTH_URL=http://localhost:7001
BETTER_AUTH_SECRET=<your-auth-secret>
```
When using D1, you do NOT need `DATABASE_URL` in `.dev.vars` — the D1 binding
handles database access automatically.
## 6. Verify Locally
```bash
cd apps/tanstack-app
pnpm dev:cf
```
The app should start on the displayed port. Verify:
1. Home page loads
2. Login works (if data was seeded)
3. Check browser console and terminal for errors
## D1 Limitations
| Limit | Value |
|-------|-------|
| Storage | 5 GB (free) / 10 GB (paid) |
| Max row size | 1 MB |
| Write concurrency | Single writer |
| No JSONB | Use `text` + JSON serialization (already handled) |
| No PG operators | `->`, `->>`, `@>` not available (already handled) |
@@ -0,0 +1,95 @@
# Cloudflare Email Sending Setup
TinyShip uses Cloudflare's Email Sending REST API for transactional emails
(verification codes, password resets, etc.). This is NOT Email Routing or
Workers email bindings — it's a simple HTTP API that works from any environment.
## 1. Enable Email Sending
In the Cloudflare Dashboard:
1. Go to **Email** → **Email Sending**
2. Add and verify a sending domain (e.g., `yourdomain.com`)
3. Follow the DNS record setup (SPF, DKIM, DMARC)
4. Wait for domain verification to complete
## 2. Create API Token
1. Go to **My Profile** → **API Tokens** → **Create Token**
2. Use **Custom token** with the permission: **Account / Email Routing Addresses: Edit**
(this also covers Email Sending)
3. Save the token
Or reuse an existing `CLOUDFLARE_API_TOKEN` that has email permissions.
## 3. Configure Environment Variables
Add to your `.env` (monorepo root):
```env
CLOUDFLARE_ACCOUNT_ID=<your-account-id>
CLOUDFLARE_API_TOKEN=<api-token-with-email-permission>
EMAIL_DEFAULT_FROM=[email protected]
```
The `EMAIL_DEFAULT_FROM` must be an address on the domain you verified in step 1.
`CLOUDFLARE_ACCOUNT_ID` is shared with R2 — if already set, no need to add it again.
## 4. Switch Default Email Provider
Edit `config/email.ts` in the TinyShip project:
```typescript
defaultProvider: 'cloudflare', // Change from 'resend' to 'cloudflare'
```
This is a code change, not just an env var. The agent should make this edit.
## 5. For Workers Deployment
Add email credentials as wrangler secrets:
```bash
cd apps/tanstack-app
npx wrangler secret put CLOUDFLARE_API_TOKEN
npx wrangler secret put EMAIL_DEFAULT_FROM
```
`CLOUDFLARE_ACCOUNT_ID` can go in `wrangler.jsonc` `vars` (not sensitive):
```jsonc
"vars": {
"CLOUDFLARE_ACCOUNT_ID": "<your-account-id>"
}
```
## 6. Verify
Run the built-in email test:
```bash
pnpm email:test -- verification --to [email protected] --provider cloudflare
```
This sends a test verification email using the Cloudflare provider.
## How It Works
The Cloudflare email provider (`libs/email/providers/cloudflare.ts`) sends emails via:
```
POST https://api.cloudflare.com/client/v4/accounts/{accountId}/email/sending/send
```
Email templates are precompiled from MJML to HTML at build time via
`pnpm email:compile`. This is important for Workers — MJML cannot run at
runtime in the Workers environment.
## Troubleshooting
| Issue | Fix |
|-------|-----|
| 403 Forbidden | API token lacks email permission |
| Domain not verified | Complete DNS setup in Dashboard → Email → Email Sending |
| MJML error on Workers | Run `pnpm email:compile` before deploying |
@@ -0,0 +1,119 @@
# Hyperdrive + PostgreSQL Setup
Hyperdrive is Cloudflare's database acceleration proxy that enables Workers to connect
to your existing PostgreSQL database via connection pooling and edge caching.
## 1. Prepare PostgreSQL Connection String
You need a PostgreSQL database accessible from the internet. The connection string format:
```
postgresql://user:password@host:5432/dbname
```
Common providers: Supabase, Neon, Railway, AWS RDS, self-hosted with public IP.
**Auto-detect local PostgreSQL:**
Before asking the user for a connection string, check if PostgreSQL is running locally:
```bash
pg_isready 2>/dev/null || \
/opt/homebrew/opt/postgresql@*/bin/pg_isready 2>/dev/null || \
/usr/local/opt/postgresql@*/bin/pg_isready 2>/dev/null
```
If running locally, the local connection string can be used for `localConnectionString`
in wrangler config (for local CF dev). The remote Hyperdrive still needs an
internet-accessible PostgreSQL for production deployment.
## 2. Create Hyperdrive Configuration
```bash
cd apps/tanstack-app
npx wrangler hyperdrive create tinyship-db \
--connection-string="postgresql://user:password@host:5432/dbname"
```
Capture the returned **Hyperdrive ID** from the output.
## 3. Configure wrangler.jsonc
Edit `apps/tanstack-app/wrangler.jsonc`:
1. **Add the `hyperdrive` binding** with the ID from step 2
2. **Set `localConnectionString`** for local CF development (your local PG)
3. **Remove or comment out `d1_databases` block** and `DB_DIALECT` var
4. **Remove `DB_DIALECT` from `vars`** (defaults to `pg`)
```jsonc
{
"hyperdrive": [
{
"binding": "HYPERDRIVE",
"id": "<your-hyperdrive-id>",
"localConnectionString": "postgresql://youruser@localhost:5432/tinyship"
}
],
"vars": {
"APP_BASE_URL": "http://localhost:7001",
"BETTER_AUTH_URL": "http://localhost:7001"
}
// Remove or comment out "d1_databases" block
// Remove "DB_DIALECT": "d1" from vars (pg is default)
}
```
**How it works:**
- **Production**: Workers use the Hyperdrive proxy (the `id`) to reach your remote PG
- **Local `pnpm dev:cf`**: Wrangler uses `localConnectionString` to connect directly
to your local database, avoiding network latency during development
- **Fallback**: If Hyperdrive binding is unavailable, the code falls back to `DATABASE_URL`
## 4. Configure .dev.vars
```bash
cd apps/tanstack-app
cp .dev.vars.example .dev.vars
```
Edit `.dev.vars`:
```
APP_BASE_URL=http://localhost:7001
BETTER_AUTH_URL=http://localhost:7001
BETTER_AUTH_SECRET=<your-auth-secret>
DATABASE_URL=postgresql://youruser@localhost:5432/tinyship
```
The `DATABASE_URL` here serves as a fallback when Hyperdrive is not available.
## 5. Push Schema and Seed
If the database is fresh, push schema and seed from the monorepo root:
```bash
# From monorepo root
pnpm db:push
pnpm db:seed
```
## 6. Verify Locally
```bash
cd apps/tanstack-app
pnpm dev:cf
```
Verify:
1. Home page loads
2. Login with `[email protected]` / `admin123` works
3. Dashboard shows data from PostgreSQL
4. No `TCP connection` errors in the terminal
## Troubleshooting
| Issue | Fix |
|-------|-----|
| `Cannot read properties of undefined (reading 'connect')` | Hyperdrive not configured; check `wrangler.jsonc` binding |
| Connection timeout in local dev | Verify `localConnectionString` points to a running PG |
| `CF_FORCE_DIRECT_DB=1` | Debug flag to skip Hyperdrive and use `DATABASE_URL` directly |
@@ -0,0 +1,100 @@
# R2 Storage Setup
Cloudflare R2 is S3-compatible object storage. TinyShip uses the S3 HTTP API
(not Workers bindings), so R2 works across all three apps — not just Workers.
## 1. Create R2 Bucket
In the Cloudflare Dashboard:
1. Go to **R2 Object Storage** → **Create bucket**
2. Name it (e.g., `tinyship`) — this becomes the `R2_BUCKET` value
3. Choose a location hint (optional)
Or via CLI:
```bash
npx wrangler r2 bucket create tinyship
```
## 2. Create R2 API Token
R2 uses separate S3-compatible credentials (not `CLOUDFLARE_API_TOKEN`):
1. Go to **R2 Object Storage** → **Manage R2 API tokens**
2. Click **Create API token**
3. Permissions: **Object Read & Write**
4. Specify the bucket (or all buckets)
5. Save the **Access Key ID** and **Secret Access Key**
## 3. Configure Environment Variables
Add to your `.env` (monorepo root):
```env
STORAGE_PROVIDER=r2
CLOUDFLARE_ACCOUNT_ID=<your-account-id>
R2_ACCESS_KEY_ID=<r2-api-token-access-key-id>
R2_ACCESS_KEY_SECRET=<r2-api-token-secret-access-key>
R2_BUCKET=tinyship
```
The `CLOUDFLARE_ACCOUNT_ID` is shared with Email Sending — if already set, no need
to add it again.
### Optional: Public URL
If you set up a custom domain for your R2 bucket (Dashboard → R2 → bucket → Settings
→ Custom Domains), add:
```env
R2_PUBLIC_URL=https://cdn.yourdomain.com
```
This enables permanent public URLs instead of time-limited signed URLs.
## 4. For Workers Deployment
When deploying to Workers, add R2 credentials as secrets:
```bash
cd apps/tanstack-app
npx wrangler secret put R2_ACCESS_KEY_ID
npx wrangler secret put R2_ACCESS_KEY_SECRET
npx wrangler secret put R2_BUCKET
```
Or set them in `wrangler.jsonc` `vars` (for non-sensitive values like bucket name):
```jsonc
"vars": {
"STORAGE_PROVIDER": "r2",
"R2_BUCKET": "tinyship"
}
```
## 5. Verify
Test the upload endpoint:
```bash
# Start the app
pnpm dev # or pnpm dev:cf for Workers mode
# Try uploading via the UI at http://localhost:7001/{lang}/upload
```
Check that files appear in your R2 bucket in the Cloudflare Dashboard.
## How It Works
R2 is accessed via S3-compatible HTTP API in `libs/storage/providers/s3.ts`:
```
Endpoint: https://{accountId}.r2.cloudflarestorage.com
Region: auto
ForcePathStyle: true
```
The `createR2Provider()` function builds an S3 client pointed at R2's endpoint.
No Workers bindings are used — this works identically on Node.js and Workers.
@@ -0,0 +1,81 @@
# Turnstile Bot Protection Setup
Cloudflare Turnstile is a CAPTCHA alternative that protects login, signup,
and password reset forms from bots.
## 1. Create Turnstile Widget
In the Cloudflare Dashboard:
1. Go to **Turnstile** → **Add Widget**
2. Name: e.g., "TinyShip"
3. Add your domains (e.g., `yourdomain.com`, `localhost`)
4. Widget mode: **Managed** (recommended)
5. Save and copy the **Site Key** and **Secret Key**
## 2. Configure Environment Variables
Add to your `.env` (monorepo root):
```env
TURNSTILE_SECRET_KEY=<your-secret-key>
NEXT_PUBLIC_TURNSTILE_SITE_KEY=<your-site-key>
```
Notes:
- `NEXT_PUBLIC_TURNSTILE_SITE_KEY` is used by Next.js client-side code (the `NEXT_PUBLIC_`
prefix is required for client exposure). TanStack Start and Nuxt also read this value.
- In development mode, Turnstile automatically falls back to Cloudflare test keys,
so you don't need real keys for local dev.
## 3. Enable Turnstile in Code
Edit `config/captcha.ts` in the TinyShip project:
```typescript
enabled: true, // Change from false to true
```
This is a code change. The agent should make this edit.
When `enabled: true`, Turnstile widgets appear on:
- Login form
- Signup form
- Forgot password form
- Phone login form
- Resend verification form
## 4. For Workers Deployment
Add the secret key as a wrangler secret:
```bash
cd apps/tanstack-app
npx wrangler secret put TURNSTILE_SECRET_KEY
```
The site key can go in `wrangler.jsonc` `vars` (it's public):
```jsonc
"vars": {
"NEXT_PUBLIC_TURNSTILE_SITE_KEY": "<your-site-key>"
}
```
## 5. Verify
1. Start the app: `pnpm dev` (or `pnpm dev:cf`)
2. Navigate to the login page
3. The Turnstile widget should appear before the submit button
4. Complete the challenge and verify login works
In development mode, Turnstile uses test keys that always pass — no real
challenge is shown.
## How It Works
- **Server side**: Better Auth captcha plugin validates the Turnstile token on
every auth request (`libs/auth/auth.ts`)
- **Client side**: `@marsidev/react-turnstile` (Next/TanStack) or `vue-turnstile`
(Nuxt) renders the widget in auth forms
- **Dev mode**: Auto-fallback to Cloudflare's always-pass test keys
@@ -0,0 +1,179 @@
# Workers Deployment
## 1. Update Production URLs
Edit `apps/tanstack-app/wrangler.jsonc` — update `vars` with your production domain:
```jsonc
"vars": {
"APP_BASE_URL": "https://yourdomain.com",
"BETTER_AUTH_URL": "https://yourdomain.com"
}
```
If using a custom domain, configure it in Dashboard → Workers → your worker → Custom Domains.
Otherwise, use the default `*.workers.dev` URL.
## 2. Configure Secrets
Workers do NOT read `.env` files. All sensitive values must be set as secrets:
```bash
cd apps/tanstack-app
# Required
npx wrangler secret put BETTER_AUTH_SECRET
# If using Hyperdrive+PG (fallback connection)
npx wrangler secret put DATABASE_URL
# If using R2
npx wrangler secret put R2_ACCESS_KEY_ID
npx wrangler secret put R2_ACCESS_KEY_SECRET
# If using Cloudflare Email
npx wrangler secret put CLOUDFLARE_API_TOKEN
# If using Turnstile
npx wrangler secret put TURNSTILE_SECRET_KEY
# Payment providers (as needed)
# npx wrangler secret put STRIPE_SECRET_KEY
# npx wrangler secret put STRIPE_WEBHOOK_SECRET
# AI providers (as needed)
# npx wrangler secret put OPENAI_API_KEY
```
**Bulk import alternative:** Create a `.secrets.json` file (do NOT commit to git):
```json
{
"BETTER_AUTH_SECRET": "...",
"DATABASE_URL": "...",
"R2_ACCESS_KEY_ID": "...",
"R2_ACCESS_KEY_SECRET": "..."
}
```
```bash
npx wrangler secret bulk .secrets.json
```
## 3. Pre-compile Email Templates
If using email sending, compile MJML templates before deployment:
```bash
# From monorepo root
pnpm email:compile
```
MJML cannot run at runtime in Workers — templates must be pre-compiled to HTML.
## 4. Local Preview (Recommended)
Before deploying, do a production-like local test:
```bash
cd apps/tanstack-app
pnpm preview:cf
```
This runs a full build + `wrangler dev`, which closely simulates the production
Workers environment. Verify:
1. Home page loads
2. Login / signup works
3. Database queries succeed
4. Upload works (if R2 configured)
5. No errors in terminal
## 5. Deploy
```bash
cd apps/tanstack-app
pnpm run deploy:cf
```
This executes `CF_DEPLOY=1 vite build && wrangler deploy`. On success, it outputs:
```
https://tinyship-tanstack.<your-subdomain>.workers.dev
```
## 6. Post-Deployment Verification
```bash
# Health check
curl -s -o /dev/null -w "%{http_code}" https://your-worker-url/api/health
# View live logs
cd apps/tanstack-app
npx wrangler tail
# Check deployment history
npx wrangler deployments list
# Rollback if needed
npx wrangler rollback
```
Verify in browser:
- Home page renders correctly
- Login with test credentials works
- Database-dependent features load
## CI/CD (Optional)
### GitHub Actions
Create `.github/workflows/deploy-cf.yml`:
```yaml
name: Deploy to Cloudflare Workers
on:
push:
branches: [main]
paths:
- 'apps/tanstack-app/**'
- 'libs/**'
- 'config/**'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: npm install -g pnpm
- run: pnpm install
- name: Deploy
working-directory: apps/tanstack-app
run: pnpm run deploy:cf
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
```
## Troubleshooting
| Issue | Fix |
|-------|-----|
| Secrets not available | Workers don't read `.env` — use `wrangler secret put` |
| `Invalid hook call` during SSR | Pin `vite-tsconfig-paths` to v5.1.4 (see CF-NOTES.md) |
| `require is not defined` | Replace `require()` with `import` in all reachable code |
| CPU time limit exceeded | Upgrade to Workers Paid plan ($5/mo) for 30s CPU time |
| Upload size exceeded | Free: 100MB, Paid: 500MB — use R2 direct upload for large files |
## Workers Plan Limits
| Resource | Free | Paid ($5/mo) |
|----------|------|--------------|
| Requests | 100k/day | 10M/mo included |
| CPU time | 10ms | 30s |
| Upload body | 100MB | 500MB |
| Worker size | 3MB | 10MB |