FffStack.Client 0.1.1

The owner has unlisted this package. This could mean that the package is deprecated, has security vulnerabilities or shouldn't be used anymore.
dotnet add package FffStack.Client --version 0.1.1
                    
NuGet\Install-Package FffStack.Client -Version 0.1.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="FffStack.Client" Version="0.1.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="FffStack.Client" Version="0.1.1" />
                    
Directory.Packages.props
<PackageReference Include="FffStack.Client" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add FffStack.Client --version 0.1.1
                    
#r "nuget: FffStack.Client, 0.1.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package FffStack.Client@0.1.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=FffStack.Client&version=0.1.1
                    
Install as a Cake Addin
#tool nuget:?package=FffStack.Client&version=0.1.1
                    
Install as a Cake Tool

FFF Stack

F# Fable Full-Stack — a fullstack framework for building web applications in F# on Cloudflare.

Client   →  F# class-based components  +  FJSX templates
Server   →  F# functional DDD          +  Cloudflare Workers
Database →  Cloudflare D1 (SQLite)
Compile  →  Fable (F# → JavaScript)

Why FFF Stack?

Modern UI frameworks trend toward complexity — hooks, signals, reactive graphs. FFF Stack takes a different path:

  • UI = OOP — components are classes with encapsulated state. No prop drilling, no state lifting.
  • Domain = FP — server logic is pure functions and DDD workflows (Domain Modeling Made Functional style). Result<'T, 'E> everywhere.
  • Templates = separate files.fjsx files compiled to direct DOM instructions at build time. No virtual DOM.
  • DDD enforcedDomain/Workflows/Endpoints/ with fff check to validate layer dependencies in CI.
  • Prototype firstfff prototype Widget scaffolds a full working vertical slice (no DB) in seconds; upgrade to D1 when the model stabilises.
  • Security built-in — CSRF, rate limiting, secure cookies, password hashing, security headers out of the box.
  • One language — F# across the full stack, compiled to JavaScript via Fable.

Quick Start

Prerequisites

Tool Install
.NET 9 SDK Required for F# compilation
Bun JS runtime + package manager
fff CLI Scaffold + DDD tooling
Wrangler Cloudflare local dev + deploy
dotnet tool install -g FffStack.Cli
bun install -g wrangler

1 — Scaffold a new project

bunx create-fff-app my-app
cd my-app
bun install
dotnet tool restore   # installs Fable 4.24.0 from .config/dotnet-tools.json

2 — Start the dev server

bun run demo         # → http://localhost:3000

The dev server runs entirely in Bun — no Cloudflare account, no .NET required. It uses an in-memory mock for the database so you can iterate on the domain model immediately.

To proxy /api/* to a running wrangler dev instance:

WRANGLER_URL=http://localhost:8787 bun run demo

3 — Build your first feature

# Scaffold a full vertical slice (Domain + Workflow + Routes + FJSX) — no DB needed
fff prototype Orders

# When the model is stable, upgrade to D1
fff generate feature Orders
fff generate migration add_orders_table

Try the demo without installing anything

# Docker — no .NET, Bun, or Cloudflare account required
git clone https://github.com/alcogy/fff-stack.git
cd fff-stack
docker compose up        # → http://localhost:3000

DDD Project Structure

Every FFF Stack server follows a three-layer architecture inspired by Domain Modeling Made Functional:

src/server/
├── Domain/                    # Pure types — no side effects, no IO
│   └── Employees/
│       └── Employee.fs        # EmployeeName, WorkEmail (value objects) + Employee (entity)
│
├── Workflows/                 # Business logic — orchestrates Domain, accesses DB
│   └── Employees/
│       └── EmployeeWorkflows.fs   # getAll, getById, create, delete (with D1)
│
├── Endpoints/                 # HTTP layer — translates requests, calls Workflows
│   ├── Employees/
│   │   └── Routes.fs          # GET/POST /api/employees, DELETE /api/employees/:id
│   ├── Ws/
│   │   └── Routes.fs          # WebSocket endpoint
│   └── Pages/
│       ├── EmployeeList.fjsx  # Server-rendered employee list (stat badges + data table)
│       ├── EmployeeNew.fjsx   # Add employee form (department select, status radio)
│       ├── Templates.fs       # [<ImportAll>] bindings + render helpers
│       └── Routes.fs          # GET /, GET /employees/new, POST /employees (PRG)
│
└── Worker.fs                  # Entry point — registers all routes

Layer rules enforced by fff check:

  • Domain/ — no open FffStack.Server, no DB calls, no IO
  • Domain/<A>/ — must not import from Domain/<B>/ (bounded context isolation)
  • Workflows/ — no open App.Server.Endpoints, no HTTP types
  • Endpoints/ — calls Workflows only; direct DB calls trigger a warning

fff CLI

The fff CLI is an F# dotnet tool that scaffolds code, enforces DDD structure, and manages the project.

Install

dotnet tool install -g FffStack.Cli

New project

fff new my-app
cd my-app && bun install && bun run demo

Tip: bunx create-fff-app my-app does the same thing without requiring the fff CLI to be installed.

Prototype a feature (no DB required)

fff prototype Widget    # full vertical slice, works immediately with bun dev

Creates Domain stub + in-memory Workflow + API Routes + Form/List FJSX + wires everything in one command. The in-memory store persists per dev-server process — iterate on the domain model without touching a database. When the model stabilises, replace the workflow with D1 queries and add a migration.

Generate code

fff generate feature   Orders       # Domain + Workflows + Endpoints (3 files, D1)
fff generate model     Address       # Domain type only
fff generate form      Employee      # FJSX form from Domain type (reads field types)
fff generate list      Employee      # FJSX list/table from Domain type
fff generate page      Dashboard     # Server FJSX page
fff generate component SearchBar     # Client component + FJSX
fff generate migration add_tags      # SQL migration file (auto-numbered)

fff generate form Employee reads type Employee = { ... } from Domain/ and generates:

✓ Endpoints/Pages/EmployeeForm.fjsx   ← inputs inferred from field types
✓ Templates.fs updated                ← import + renderEmployeeForm added

Field type inference: email<input type="email">, body/description<textarea>, bool<input type="checkbox">, int/float<input type="number">, XxxAt/date<input type="date">, id/createdAt/slug → skipped automatically.

Inspect & validate

fff routes              # Colour-coded list of all registered HTTP routes
fff check               # Validate DDD layer dependencies + bounded context isolation (CI-ready)
fff context             # Bounded context map — dependency graph + shared language terms
fff context --mermaid   # Output Mermaid graph definition for docs
fff doctor              # Project health: tools, fsproj, env vars, migrations
fff openapi             # Generate openapi.yaml from route definitions
fff openapi -o api.yaml # Write to specific path

fff check example:

✗ Domain/Orders/Orders.fs:17 — forbidden import 'open FffStack.Server'
✗ Domain/Users/User.fs:12 — Domain/Users imports Domain/Posts — use Anti-Corruption Layer
2 violation(s) found.

fff context example:

fff context — Bounded Context Map

Contexts (2):
  ● Employees    (3 types)
  ● Users        (2 types)

✓ Domain boundaries are clean — no cross-context dependencies.

Run 'fff context --mermaid' for a Mermaid diagram you can paste into docs.

fff doctor checks:

  • .NET SDK, Bun, Wrangler versions
  • All .fs files declared in Server.fsproj
  • Env.require* bindings declared in wrangler.toml
  • Migration file count
  • Domain layer dependency violations

Database

fff migrate               # Apply pending migrations (local D1)
fff migrate --remote      # Apply to production D1
fff migrate:status        # Show pending/applied status

FJSX Templates

Templates live in separate .fjsx files compiled to direct DOM instructions at build time:


<div class="counter">
  <p>Count: {this.Count}</p>
  <button onclick={this.Decrement}>−</button>
  <button onclick={this.Increment}>+</button>
</div>

<style>
  .counter { display: flex; gap: 0.5rem; align-items: center; }
</style>

Directives


{#each this.Items as item}
  <li>{item.name}</li>
{/each}


{#each this.Items as item (item.id)}
  <li>{item.name}</li>
{/each}


{#if this.IsLoggedIn}
  <span>Welcome, {this.UserName}</span>
{:else}
  <a href="/login">Sign in</a>
{/if}


{@this.HtmlContent}


<input bind:value={this.Name} />
<input type="checkbox" bind:checked={this.Active} />
<select bind:value={this.Role}>...</select>


<form onsubmit|prevent={this.Submit}>...</form>
<button onclick|stop={this.Handle}>...</button>
<a onclick|prevent|stop={this.Navigate}>...</a>


<div class="card">
  <slot />
</div>


<slot name="footer" />   

Compilation output

export function init(component, container)       { /* build DOM once */ }
export function hydrate(component, container)    { /* attach to SSR HTML */ }
export function renderToHtml(component)          { /* SSR */ }
export function renderToHydration(component)     { /* SSR with hydration markers */ }

Error messages include source line and pointer:

Counter.fjsx:3:14: ParseError: Expected > or /> after <button ...>

  <button onclik={this.Increment}>
              ^

Component Model

type Counter(containerId: string) =
    inherit Component(containerId)

    let mutable _count = 0
    member val Count = _count with get, set

    member this.Increment() =
        _count <- _count + 1
        this.Count <- _count
        this.Render()

    override this.Render() =
        Template.load "Counter.fjsx" this

Lifecycle hooks

override this.OnMount()   = // called once when mounted
override this.OnUpdate()  = // called after every re-render
override this.OnDispose() = // called on unmount

True hydration (SSR → client)

// Server: render with hydration markers
let html = Ssr.renderHydration "Counter.fjsx" counter

// Client App.fs — Hydrate() when server content exists, Mount() otherwise
if el.childElementCount > 0 then comp.Hydrate()
else comp.Mount()

Shared state

let store = Store.create { Theme = "dark"; User = None }

let unsub = Store.bind store (fun s -> this.Theme <- s.Theme) this
override this.OnDispose() = unsub()

Server

Router

router
    .Get("/api/users", fun ctx -> promise {
        let db = Env.requireD1 ctx.Env "DB"
        let! users = getAll db
        return Response.json (users |> List.toArray)
    })
    .Get("/api/users/:id", fun ctx -> promise {
        let id = ctx.Params.["id"]
        match! getById (Env.requireD1 ctx.Env "DB") id with
        | None   -> return Response.notFound "Not found"
        | Some u -> return Response.json u
    })
    |> ignore

Middleware

let handler =
    SecurityHeaders.helmet             // X-Content-Type-Options, X-Frame-Options, ...
    >>> SecurityHeaders.csp [          // Content-Security-Policy
        Csp.DefaultSrc [ Csp.Self ]
        Csp.ScriptSrc  [ Csp.Self; Csp.Nonce "abc" ]
    ]
    >>> Middleware.cors ["https://myapp.com"]
    >>> Middleware.requestSizeLimit (1 * 1024 * 1024)  // 1 MB
    >>> Middleware.logger
    >>> Csrf.protect
    |> Middleware.apply router.Fetch

Rate limiting

// KV-backed (production)
RateLimit.perIp 60 60 (Env.requireKv ctx.Env "RATE_LIMIT_KV")

// In-memory (development)
RateLimit.inMemory 100 60

CSRF protection

// In a form template — embed token
let token = Csrf.token ctx

// Middleware validates X-CSRF-Token header against cookie on mutations
Csrf.protect

Cache control

Cache.control 3600   // Cache-Control: public, max-age=3600
Cache.private'       // Cache-Control: private, no-store
Cache.etag           // ETag + 304 Not Modified
Cache.vary ["Accept-Encoding"]

Pagination

// Offset-based
let p = Pagination.offsetParams ctx  // ?page=1&size=20
let! rows = queryAll<T> db (sql + Pagination.offsetSql p) [||]
return Pagination.offsetResponse rows p totalCount

// Cursor-based
let p = Pagination.cursorParams ctx  // ?cursor=xxx&limit=20
let! rows = queryAll<T> db (sql + Pagination.cursorSql "id" p) [||]
return Pagination.cursorResponse rows p (fun r -> r.id)

WebSocket

router.Get("/ws", fun ctx ->
    if not (Ws.isUpgrade ctx) then
        promise { return Response.badRequest "Expected WebSocket upgrade" }
    else
        let ws, response = Ws.accept ctx
        ws.OnMessage(fun msg -> ws.Send("Echo: " + msg))
        ws.OnClose(fun code _ -> JS.console.log(sprintf "closed: %d" code))
        promise { return response })

Authentication + Passwords

// Hash and verify passwords (PBKDF2-SHA256, 600k iterations)
let! hash  = Password.hash "my-password"
let! valid = Password.verify "my-password" hash

// Issue access + refresh token pair
let! tokens = Auth.issueTokens secret userId Map.empty 900 2592000
// tokens.AccessToken, tokens.RefreshToken, tokens.ExpiresIn

// Rotate refresh token (invalidates old token)
match! Auth.rotateTokens secret oldRefreshToken 900 2592000 with
| None      -> return Response.status 401 {| error = "Invalid refresh token" |}
| Some pair -> return Response.json pair

Cookies

// Build secure Set-Cookie header
let header = Cookie.secure "session" token 3600  // HttpOnly; Secure; SameSite=Lax

// Custom attributes
let header = Cookie.build "pref" "dark" [
    Cookie.SameSite Cookie.Strict
    Cookie.MaxAge 86400
    Cookie.Path "/"
]

// Read
let value = Cookie.get "session" ctx.Request
let all   = Cookie.getAll ctx.Request

Crypto utilities

let equal = Crypto.timingSafeEqual tokenA tokenB  // constant-time comparison
let hex   = Crypto.randomHex 32                    // 64-char hex string
let! h    = Crypto.sha256Hex "data"

Validation

let validateUser =
    Validator.required "name"
    >=> Validator.minLength "name" 2
    >=> Validator.maxLength "name" 100

match validateUser rawName with
| Ok name   -> // proceed
| Error err -> Response.badRequest (Validator.formatErrors err)

D1 Database

let! users = queryAll<User> db "SELECT * FROM users" [||]
let! user  = queryOne<User> db "SELECT * FROM users WHERE id = ?" [| id |]
do! execute db "INSERT INTO users (id, name) VALUES (?, ?)" [| id; name |]
    |> Promise.map ignore

R2 Object Storage

let! _   = R2.put bucket "avatars/user-123.jpg" body "image/jpeg"
match! R2.get bucket "avatars/user-123.jpg" with
| None     -> return Response.notFound "Not found"
| Some obj -> return R2.toResponse obj

POST form + PRG pattern

router.Post("/users", fun ctx -> promise {
    let! form = Form.parseForm ctx.Request
    match Form.field "name" form with
    | Error e    -> return Flash.redirectWith "/" "error" e
    | Ok rawName ->
        match! User.create db rawName with
        | Error e -> return Flash.redirectWith "/" "error" e
        | Ok user -> return Flash.redirectWith "/" "success"
                         (sprintf "User '%s' added" user.name)
})

Local Development with D1 / R2

bun run demo uses an in-memory mock and requires no Cloudflare account. When you're ready to develop against real D1/R2 bindings locally, use wrangler dev.

1 — Configure wrangler.toml

For local-only development, a placeholder database_id is sufficient — Wrangler emulates D1 with a local SQLite file and does not contact Cloudflare.

[[d1_databases]]
binding = "DB"
database_name = "my-app-db"
database_id = "00000000-0000-0000-0000-000000000000"  # dummy OK for local dev

[[r2_buckets]]
binding = "BUCKET"
bucket_name = "my-app-bucket"   # does not need to exist on Cloudflare

2 — Apply migrations locally

wrangler d1 execute DB --local --file=migrations/0001_init.sql
# repeat for each migration file

3 — Start the local Cloudflare Workers runtime

bun dev
# runs wrangler dev + FJSX watcher simultaneously

Or proxy the fff-stack dev server to wrangler dev:

# Terminal 1
wrangler dev

# Terminal 2
WRANGLER_URL=http://localhost:8787 bun run demo

Local D1 data is stored in .wrangler/state/d1/ and R2 in .wrangler/state/r2/.

Note: wrangler login (free Cloudflare account) is required even for local development. No D1/R2 resources need to be created on Cloudflare until you deploy.


Deployment

# 1. Create D1 database on Cloudflare
wrangler d1 create my-app-db
# → copy the database_id into wrangler.toml

# 2. Apply migrations
fff migrate

# 3. Deploy to Cloudflare Workers
bun run deploy

UI Library

The template ships a Cloudflare-inspired design system in public/style.css — no build step, zero dependencies.

/* Design tokens */
--fff-accent: #F6821F;   /* Cloudflare orange */
--fff-bg:     #F3F4F6;   /* page background */
--fff-surface: #FFFFFF;  /* cards, inputs */
/* + success/warning/danger/info with -dim variants */
/* + --fff-sidebar-* for two-column admin panel layout */

Components included: .card, .form-group, .form-row, .form-actions, .form-checkbox, .radio-group, .data-table, .list-header, .stat-bar, .badge-{success|warning|danger|info|neutral}, .flash-{success|error|warning|info}, .empty-state, .app-shell (sidebar layout), .btn-{primary|secondary|danger|ghost}.

All classes used by fff generate form and fff generate list are pre-styled — prototypes look presentable immediately.


Packages

Package Description
@fff-stack/fjsx-compiler FJSX → JS compiler + CLI (fjsx, fjsx-watcher)
FffStack.Client F# component runtime (Component, Template, Store)
FffStack.Server F# server runtime (Router, D1, R2, Auth, Validation, Security…)
FffStack.Cli fff CLI (new, prototype, generate, check, context, routes, doctor, migrate, openapi)
create-fff-app Project scaffold without .NET (bunx create-fff-app my-app)

Contributing / Monorepo Development

To work on fff-stack itself (packages, compiler, runtime):

git clone https://github.com/alcogy/fff-stack.git
cd fff-stack

# Install JS workspace packages
bun install

# Build the FJSX compiler CLI
cd packages/fjsx-compiler && bun run build && cd ../..

# Install Fable (local tool — pinned version)
cd template && dotnet tool restore && cd ..

# Run the demo
bun dev                        # → http://localhost:3000

Build targets (inside template/):

bun run build:templates        # compile *.fjsx → JS (client components)
bun run build:page-templates   # compile *.fjsx → JS (server pages)
bun run build:server           # Fable: F# server → dist/server/
bun run build:client           # Fable: F# client → dist/client/
bun run build                  # all of the above + assets

bun run test:server            # F# server-runtime tests

Publish:

# npm
cd packages/fjsx-compiler  && npm publish
cd packages/create-fff-app && npm publish   # bundles template/ automatically

# NuGet
cd packages/server-runtime && dotnet pack && dotnet nuget push **/*.nupkg ...
cd packages/client-runtime && dotnet pack && dotnet nuget push **/*.nupkg ...
cd packages/fff-cli        && dotnet pack && dotnet nuget push **/*.nupkg ...

Roadmap

v1–v3 ✅ Complete

  • FJSX compiler — lexer, parser, codegen, source maps, {#each}, {#if}, {@expr}
  • CSS <style> blocks, keyed {#each} diffing, true hydration
  • SSR — renderToHtml / renderToHydration
  • Class-based component runtime with lifecycle hooks
  • Cloudflare Workers router, D1, R2, Auth (JWT HS256), Middleware
  • POST form + PRG pattern, flash messages, client-side navigation
  • MPA page routing, Islands Architecture, Component-level HMR
  • Type-safe template bindings, FJSX source maps
  • create-fff-app scaffold, VS Code extension

v4 ✅ Complete — DDD Enforcement

  • Domain/Workflows/Endpoints/ structure
  • fff generate feature/model/page/component/migration
  • fff check — CI-ready layer dependency validator
  • Dockerfile + docker compose up

v5 ✅ Complete — Full-Stack Features

  • FJSX: bind:value, event modifiers (|prevent|stop), <slot> composition
  • WebSocket support — Ws.accept, WebSocketHandle, Ws.broadcast
  • CSRF protection, rate limiting (KV + in-memory), cache control, pagination
  • fff new, fff routes

v6 ✅ Complete — Security + DX

  • Security headers — SecurityHeaders.helmet, typed CSP builder, HSTS
  • Secure cookies — Cookie.build/secure/clear/get with typed attributes
  • Crypto — timingSafeEqual, randomHex, sha256Hex
  • Password hashing — PBKDF2-SHA256 (600k iterations)
  • JWT refresh tokens — Auth.issueTokens, Auth.rotateTokens
  • Request size limit middleware — Middleware.requestSizeLimit
  • fff migrate — D1 migration runner
  • fff doctor — project health check
  • FJSX error messages — line/col + source excerpt with ^ pointer
  • fff openapi — OpenAPI 3.0.3 spec generation

v7 ✅ Complete — FP-DDD Business App Focus

  • Type-Driven Scaffolding — fff generate form/list reads F# record types and generates FJSX
  • Bounded Context Tooling — fff context / fff context --mermaid, fff check boundary isolation
  • Prototyping support — fff prototype full vertical slice with in-memory workflow
  • Cloudflare-inspired UI kit — CSS custom properties, all scaffold classes pre-styled
  • Template redesign — Employee Directory (business CRUD demo with PRG, validation, stat badges)

v8 ✅ Complete — OSS Packaging

  • ProjectReferencePackageReference in template (NuGet packages)
  • create-fff-app bundles template at publish time (prepublishOnly)
  • dev-server.ts imports @fff-stack/fjsx-compiler via npm package
  • fff new resolves template from globally installed create-fff-app

Next

  • PostgreSQL / MySQL support
  • AWS Lambda / on-premises infrastructure adapter
  • File-based routing
  • OAuth2 / social login (GitHub, Google)
  • RBAC middleware
  • fff prototype upgrade path — fff upgrade <Name> converts in-memory to D1 workflow

License

MIT

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated