Fidelity.CloudEdge will implement code-first Infrastructure as Code (IaC) where the primary configuration method is expressed as F# code, as opposed to managing a fragmented array of TOML files. The framework makes code-driven migrations and deployments a primary mechanism. While we may provide wrangler.toml export for backward compatibility with existing Cloudflare tooling, static configuration diverges from our vision of application/solution-level code-driven infrastructure.
After analyzing the wrangler source code (from cloudflare/workers-sdk), deploying directly via REST APIs using F# configuration scripts is not only feasible but architecturally consistent. Fidelity.CloudEdge already has the foundation for this with the Management API layer, and the wrangler source reveals the exact metadata structure needed.
F# .fsx configuration of solutions is our choice for deployment and configuration management. All migrations and deployments should be code-driven, and other code-driven approaches may be considered in the future.
Fidelity.CloudEdge supports multiple deployment modes to fit different workflows - from direct API deployment to offline TOML generation for traditional CI/CD pipelines.
// deploy.fsx - Direct deployment via Cloudflare APIs
#r "nuget: Fidelity.CloudEdge"
open Fidelity.CloudEdge.Api
open Fidelity.CloudEdge.Deployment
let deploy() = cloudflare {
account "abc123"
worker "api-service" {
// Resources are created automatically
kv "CACHE" (ensureNamespace "api-cache")
r2 "STORAGE" (ensureBucket "api-storage")
d1 "DB" (ensureDatabase "api-db")
route "api.example.com/*"
script "./dist/worker.js"
}
}
// Execute: cfs deploy ./deploy.fsx
// Result: Direct deployment via API calls// deploy.fsx - Same script, different output mode
let deploy() = cloudflare {
account "abc123"
worker "api-service" {
// When offline, assumes resources exist
kv "CACHE" (useExisting "kv-namespace-id-123")
r2 "STORAGE" (useExisting "my-bucket")
d1 "DB" (useExisting "d1-database-id-456")
route "api.example.com/*"
script "./dist/worker.js"
}
}
// Execute: cfs deploy ./deploy.fsx --offline
// Result: Generates wrangler.tomlGenerated wrangler.toml:
name = "api-service"
main = "./dist/worker.js"
compatibility_date = "2023-10-01"
[[kv_namespaces]]
binding = "CACHE"
id = "kv-namespace-id-123"
[[r2_buckets]]
binding = "STORAGE"
bucket_name = "my-bucket"
[[d1_databases]]
binding = "DB"
database_id = "d1-database-id-456"
[[routes]]
pattern = "api.example.com/*"// deploy.fsx - Provision resources, then generate TOML
let deploy() = deployment {
mode Hybrid // Provision resources via API, generate TOML for deployment
account "abc123"
// Phase 1: Provision resources (via API)
provision {
kv "api-cache"
kv "api-sessions"
r2 "api-storage"
d1 "api-database" "./migrations"
}
// Phase 2: Generate TOML with provisioned IDs
worker "api-service" {
kv "CACHE" (fromProvisioned "api-cache")
kv "SESSIONS" (fromProvisioned "api-sessions")
r2 "STORAGE" (fromProvisioned "api-storage")
d1 "DB" (fromProvisioned "api-database")
route "api.example.com/*"
}
}
// Execute: cfs deploy ./deploy.fsx --hybrid
// Result:
// 1. Creates resources via API
// 2. Generates wrangler.toml with real IDs
// 3. User runs: wrangler deployThe Core Upload API:
PUT /accounts/{account_id}/workers/scripts/{script_name}/content
Content-Type: multipart/form-data
--boundary
Content-Disposition: form-data; name="metadata"
Content-Type: application/json
{
"main_module": "worker.js",
"compatibility_date": "2024-01-01",
"bindings": [
{
"type": "kv_namespace",
"name": "CACHE",
"namespace_id": "abc123"
},
{
"type": "r2_bucket",
"name": "STORAGE",
"bucket_name": "my-bucket"
},
{
"type": "d1_database",
"name": "DB",
"id": "database-uuid"
}
],
"routes": [
{
"pattern": "example.com/*",
"zone_id": "zone-123"
}
]
}
--boundary
Content-Disposition: form-data; name="worker.js"; filename="worker.js"
Content-Type: application/javascript
// Your compiled worker code here
export default {
async fetch(request, env, ctx) {
// Worker implementation
}
}
--boundary--// Fidelity.CloudEdge Direct Deployment
module Fidelity.CloudEdge.Deployment.Direct
open System.Net.Http
open System.Text
// Complete binding types discovered from wrangler source
type WorkerBinding =
| KVNamespace of name: string * namespaceId: string
| R2Bucket of name: string * bucketName: string * jurisdiction: string option
| D1Database of name: string * id: string
| Queue of name: string * queueName: string * deliveryDelay: int option
| DurableObject of name: string * className: string * scriptName: string option
| Vectorize of name: string * indexName: string
| Hyperdrive of name: string * id: string
| AI of name: string * staging: bool option
| Browser of name: string
| AnalyticsEngine of name: string * dataset: string option
| Service of name: string * service: string * environment: string option
| MTLSCertificate of name: string * certificateId: string
| Assets of name: string // Unified static asset system
type WorkerMetadata = {
MainModule: string
CompatibilityDate: string
Bindings: WorkerBinding list
Routes: Route list
UsageModel: string option
}
type DirectDeployer(httpClient: HttpClient, accountId: string) =
member this.DeployWorker(scriptName: string, code: byte[], metadata: WorkerMetadata) = async {
// 1. Create multipart form content
use content = new MultipartFormDataContent()
// 2. Add metadata JSON
let metadataJson = JsonSerializer.Serialize(metadata)
content.Add(new StringContent(metadataJson, Encoding.UTF8, "application/json"), "metadata")
// 3. Add worker code
content.Add(new ByteArrayContent(code), "worker.js", "worker.js")
// 4. Upload directly via API
let url = $"https://api.cloudflare.com/client/v4/accounts/{accountId}/workers/scripts/{scriptName}/content"
let! response = httpClient.PutAsync(url, content) |> Async.AwaitTask
return response.IsSuccessStatusCode
}// deploy.fsx
open Fidelity.CloudEdge
let getDeploymentMode() =
match Environment.GetEnvironmentVariable("CF_DEPLOY_MODE") with
| "offline" -> Offline
| "hybrid" -> Hybrid
| "dry-run" -> DryRun
| _ -> Direct
let config env = cloudflare {
account (getAccountId env)
worker $"api-service-{env}" {
match env with
| "production" ->
// Production uses existing resources
kv "CACHE" (useExisting "prod-cache-id")
r2 "STORAGE" (useExisting "prod-storage")
d1 "DB" (useExisting "prod-db-id")
| "staging" ->
// Staging provisions new resources
kv "CACHE" (ensureNamespace "staging-cache")
r2 "STORAGE" (ensureBucket "staging-storage")
d1 "DB" (ensureDatabase "staging-db")
| "development" ->
// Dev uses local resources
kv "CACHE" (local "./kv-data")
r2 "STORAGE" (local "./r2-data")
d1 "DB" (local "./db.sqlite")
}
}
// Deploy based on mode
match getDeploymentMode() with
| Direct ->
config "production"
|> deployDirect client
| Offline ->
config "production"
|> generateToml "wrangler.toml"
| Hybrid ->
config "production"
|> provisionResources client
|> generateToml "wrangler.toml"type ResourceRef =
| Ensure of name: string // Create if doesn't exist
| UseExisting of id: string // Use specific ID
| FromProvisioned of name: string // Use ID from provision phase
| FromConfig of key: string // Read from config file
| FromEnvironment of var: string // Read from env var
let resolveResource (client: CloudflareClient option) accountId ref =
match ref, client with
| Ensure name, Some client ->
// Online mode: ensure exists via API
async {
let! existing = client.KV.ListNamespaces(accountId)
match existing |> Array.tryFind (fun ns -> ns.title = name) with
| Some ns -> return ns.id
| None ->
let! created = client.KV.CreateNamespace(accountId, name)
return created.id
}
| UseExisting id, _ ->
// Offline mode: use provided ID
async { return id }
| FromEnvironment var, _ ->
// Read from environment
async { return Environment.GetEnvironmentVariable(var) }
| FromConfig key, _ ->
// Read from config file
async {
let config = loadConfig "config.json"
return config.[key]
}
| _, None ->
failwith "Client required for resource provisioning"After examining the wrangler source code, wrangler functions as a convenience wrapper that:
- Reads wrangler.toml and converts it to API calls
- Bundles JavaScript/WASM using esbuild internally
- Uploads via multipart form with JSON metadata containing all bindings
- Manages authentication via Bearer tokens
- Provides local dev server (miniflare integration)
- Handles complex orchestration:
- Automatic retries on API failures
- Bundle size validation
- Source map management
- Module dependency analysis
- Resource provisioning (auto-creates if missing)
- Durable Object migrations
- Asset manifest generation and differential uploads
The wrangler source reveals all binding types that can be specified in metadata:
- Standard storage:
kv_namespace,r2_bucket,d1 - Advanced services:
queue,vectorize,hyperdrive - AI/ML:
ai,browser(for Browser Rendering API) - Networking:
service(service bindings),mtls_certificate - Analytics:
analytics_engine - Assets:
assets- Unified static asset system replacing Workers Sites
- Versioning API:
/content/v2?version={versionId}for deployment versions - Keep Bindings: Selective preservation of existing bindings during updates
- Tail Consumers: Built-in log streaming configuration
- Placement Hints: Internal optimization directives
- Usage Models:
bundledvsunboundpricing models - Compatibility Flags: Beyond just dates, specific feature flags
The assets binding discovered in wrangler source provides a simpler approach:
// Workers Sites approach (KV-based)
type WorkersSitesConfig = {
bucket: string
include: string list
exclude: string list
// Requires separate @cloudflare/kv-asset-handler package
}
// Assets binding approach (platform-native)
type AssetsBinding = {
type: "assets"
name: string // e.g., "ASSETS"
// Assets handled natively by platform
}- In Metadata (no TOML needed):
{
"bindings": [
{"type": "assets", "name": "ASSETS"}
],
"assets": {
"config": {
"html_handling": "auto-trailing-slash",
"not_found_handling": "404-page"
},
"jwt": "<from-asset-upload-session>"
}
}- In Worker (F# via Fable):
type Env = {
ASSETS: Fetcher // Regular Fetcher
DATABASE: D1Database
CACHE: KVNamespace
}
let fetch (req: Request) (env: Env) = async {
// Assets are just another binding
match req.url.pathname with
| StartsWith "/api" -> handleApi req env
| _ -> env.ASSETS.fetch(req) // Serve static assets
}- Unified Deployment: Worker + Assets in single API call
- No KV Storage: Direct CDN integration (faster, cheaper)
- Native Platform Feature: No external packages needed
- Full Programmatic Control: Assets are Fetch API responses
- Advanced Routing:
run_worker_firstoption for full control
Problem: Need to know KV namespace IDs, R2 bucket names, D1 database IDs Solution: Fidelity.CloudEdge Management APIs already handle this:
// Provision resources and get IDs
let! kvNamespace = kvClient.CreateNamespace(accountId, "my-cache")
let! r2Bucket = r2Client.CreateBucket(accountId, "my-storage")
let! d1Database = d1Client.CreateDatabase(accountId, "my-db")
// Use IDs in deployment
let bindings = [
KVNamespace("CACHE", kvNamespace.id)
R2Bucket("STORAGE", r2Bucket.name)
D1Database("DB", d1Database.uuid)
]Problem: Need to bundle TypeScript/JavaScript before upload Solution: Multiple options:
- Use Fable output directly (already bundled)
- Shell out to esbuild when needed
- Use .NET JavaScript bundling libraries
Problem: Wrangler provides local dev server Solution:
- Keep using
wrangler devfor local development - OR implement Miniflare bindings in F#
- OR use Cloudflare's preview deployments
# Direct deployment (default)
cfs deploy ./deploy.fsx
# Generate TOML only
cfs deploy ./deploy.fsx --offline
# Provision resources and generate TOML
cfs deploy ./deploy.fsx --hybrid
# Dry run - show what would happen
cfs deploy ./deploy.fsx --dry-run
# Generate TOML with specific output
cfs deploy ./deploy.fsx --offline --output ./ci/wrangler.toml# Use specific account
cfs deploy ./deploy.fsx --account abc123
# Override environment
cfs deploy ./deploy.fsx --env production
# Validate without deploying
cfs validate ./deploy.fsx
# Show resource diff
cfs diff ./deploy.fsx --with-remote
# Generate multiple TOMLs for different environments
cfs deploy ./deploy.fsx --offline --multi-env
# Outputs: wrangler.development.toml, wrangler.staging.toml, wrangler.production.tomlname: Deploy to Cloudflare
on:
push:
branches: [main]
jobs:
deploy-hybrid:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0'
- name: Install Fidelity.CloudEdge CLI
run: dotnet tool install -g Fidelity.CloudEdge.CLI
- name: Provision Resources and Generate TOML
env:
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
run: |
cfs deploy ./deploy.fsx --hybrid
- name: Deploy with Wrangler
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
wranglerVersion: '3.0.0'deploy:
stage: deploy
script:
# Generate TOML for offline deployment
- cfs deploy ./deploy.fsx --offline
# Use generated TOML with wrangler
- wrangler deploy --config wrangler.toml
only:
- main✅ No TOML files to maintain ✅ Dynamic resource provisioning ✅ Full programmatic control ✅ Immediate feedback ❌ Requires API access from deployment environment
✅ Works with existing CI/CD pipelines ✅ No API access needed during deployment ✅ Version control friendly ✅ Compatible with Wrangler workflows ❌ Manual resource ID management
✅ Best of both worlds ✅ Automated resource provisioning ✅ TOML for deployment compatibility ✅ Reduces configuration drift ❌ Two-phase deployment process
// Easiest migration - generate TOML from F#
let config = parseExistingWrangler "wrangler.toml"
|> toFidelity.CloudEdge
|> enhance // Add type safety
generateToml config "wrangler-new.toml"// Provision new resources via API, keep TOML deployment
let config = cloudflare {
importExisting "wrangler.toml"
// Add new resources via API
kv "NEW_CACHE" (ensureNamespace "new-cache")
}
config |> hybrid client // Provisions then generates TOML// Eventually move to pure API deployment
let config = cloudflare {
// Everything defined in F#
// No TOML needed
}
config |> deploy client// Simple F# script deployment
cfs deploy-direct ./worker.js --bindings ./bindings.fsx// Full F# deployment script - this IS the configuration
// No TOML or YAML needed - code is the single source of truth
#r "nuget: Fidelity.CloudEdge"
let deploy() = cloudflare {
// Provision resources - all in F# code
let! kv = ensureKVNamespace "cache"
let! r2 = ensureR2Bucket "storage"
let! d1 = ensureD1Database "database"
// Deploy worker with bindings - pure F# computation expression
worker "my-api" {
code "./dist/worker.js"
bindings [
bind "CACHE" kv
bind "STORAGE" r2
bind "DB" d1
]
routes ["api.example.com/*"]
}
// Optional: Export wrangler.toml for secondary tooling compatibility
// This is a backward compatibility feature, not the primary workflow
exportWranglerCompat "./wrangler.toml"
}- Implement all wrangler features in F#
- Local dev server integration
- Tail logging
- Secret management
- Cron triggers
A minimal implementation requires:
- Complete Workers Management API binding (currently empty)
- Multipart form upload helper
- Metadata serializer for bindings
- Simple CLI command
// Minimal POC
type CloudflareDirect(apiToken: string, accountId: string) =
let client = new HttpClient()
do client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiToken}")
member this.QuickDeploy(scriptName, jsCode: string, bindings) = async {
// This would work with current APIs
let metadata = {|
main_module = "worker.js"
compatibility_date = "2024-01-01"
bindings = bindings
|}
// ... multipart upload implementation
}- Workers Upload Client (Priority 1):
type WorkerDeployment = {
Script: byte[] // Fable output
Assets: AssetManifest option // Static files
Bindings: WorkerBinding list
Routes: Route list
CompatibilityDate: string
CompatibilityFlags: string list
}
let deployDirectly (deployment: WorkerDeployment) = async {
// 1. Upload assets if present
let! assetJwt =
match deployment.Assets with
| Some assets -> uploadAssets accountId scriptName assets
| None -> async { return None }
// 2. Create metadata with ALL binding types
let metadata = createMetadata deployment assetJwt
// 3. Single multipart upload
return! uploadWorker accountId scriptName deployment.Script metadata
}-
Keep Wrangler For:
- Local development (
wrangler devwith hot reload) - Complex JavaScript bundling scenarios
- Initial project scaffolding
- Local development (
-
Replace Wrangler For:
- CI/CD deployments (direct API faster)
- F# script-based deployments
- Multi-environment orchestration
- Production deployments with full control
Core Approach: Fidelity.CloudEdge implements code-first IaC as a primary feature. Configuration files like TOML and YAML are treated as secondary formats. While we may provide export functionality for compatibility, the primary approach is pure F# code configuration.
Short Term:
- Implement direct deployment via F# scripts for all scenarios
- Support wrangler.toml export for secondary tool compatibility
- Keep wrangler for local development
Long Term:
- Feature parity with wrangler functionality
- Direct API calls for everything via F# code
- No configuration files needed - pure code-driven infrastructure
- F# scripts as the single source of truth for all deployments
Fidelity.CloudEdge's code-first approach provides a viable alternative for Cloudflare infrastructure management. After analyzing wrangler's source code, we've confirmed that static configuration files like TOML are unnecessary for the purposes of this toolkit and can be eliminated in favor of direct F# code managed deployment to forward environments.
The discoveries reveal:
- Complete Binding Inventory: Every binding can be expressed as F# code, not TOML
- Assets Binding: The new
assetsbinding integrates well with code-first deployment - Direct API Access: F# scripts can call APIs directly without TOML intermediaries
- No Configuration Files Required: Everything is achievable through code
The wrangler source code confirms our code-first approach is viable because:
- All APIs accept programmatic input (suitable for F# scripts)
- No static files are actually required by Cloudflare's APIs
- Everything can be computed and deployed via code
Fidelity.CloudEdge already has most of what's needed. The missing components are:
- Workers Script Upload API binding (for direct F# script deployment)
- Asset manifest generation (code-driven, not config-driven)
- CLI commands that execute F# scripts directly
Immediate Next Steps:
- Implement F# script-based deployment without config files
- Create code-driven asset manifest generation
- Build POC that shows pure F# deployment (with optional wrangler.toml export)
- Demonstrate code-first IaC implementation
Fidelity.CloudEdge provides flexible deployment modes:
- Direct - Pure API deployment for maximum control
- Offline - TOML generation for compatibility
- Hybrid - Resource provisioning + TOML generation
This allows gradual migration from TOML-based workflows to full API-driven deployment while maintaining compatibility with existing tools and pipelines.