v0.1.0.NET 10 · six packages · MIT

Building blocks for small sites behind Cloudflare

The parts every ASP.NET Core and Blazor site on Azure Container Apps needs and nobody wants to write twice: a job queue in PostgreSQL, origin protection, cache headers, Turnstile, Tailwind without Node, image uploads. The web sibling of Deskling.

dotnet add package Webling.Jobs
Program.cs
using Webling.AspNetCore;
using Webling.Cloudflare;

builder.AddCloudflareOrigin();
builder.Services.AddTurnstile(config.GetSection("Turnstile"));
builder.Services.AddPublicCaching("__Host-session");

var app = builder.Build();
// 403 unless the request came through your zone.
app.UseCloudflareOrigin();
app.UseSecurityHeaders(nonce =>
    $"script-src 'self' 'nonce-{nonce}'");
app.UseAuthentication();
// [PublicPage] components: cached at the edge,
// private for signed-in visitors.
app.UsePublicPageCaching();

Safe when misconfigured

No origin secret outside Development: the app won't start. Turnstile unreachable: the check fails. No purge token: nothing is sent. A missing setting never turns into an open door.

Your names, your strings

Cookies, paths, job names and secrets come from your app and its configuration. The packages ship no text, and the job table uses explicit names whatever your EF naming convention is.

Tested against the real thing

The job queue runs against PostgreSQL, the middleware on TestServer, the Tailwind build on Linux, Windows and macOS. Cloudflare's IP list is checked every week.

What's inside

Six packages, one version. Take the job queue alone, or the whole stack from AppHost to stylesheet.

Package Depends on What it does
Webling.Jobs
EF Core

Background jobs in PostgreSQL

Enqueue in the same transaction as the change that caused the job. Replicas claim jobs with FOR UPDATE SKIP LOCKED, hold a 5-minute lease, retry with backoff and give the attempt back on shutdown. Old finished jobs are cleaned up.

EnqueueJob IJob IJobHandler<T> AddJobDispatcher<TContext> ApplyWeblingJobs

Webling.AspNetCore
ASP.NET Core

Origin, headers and caching

An origin check with a shared secret, health endpoints hidden from the edge, the client IP trusted only after the check. Security headers with a nonce per request. Edge caching for public pages that turns private when a personal cookie is present.

UseCloudflareOrigin UseSecurityHeaders GetCspNonce [PublicPage] PublicCaching

Webling.Cloudflare
HttpClient

Turnstile and cache purges

Verify a Turnstile token behind an interface your use cases can depend on. Purge changed pages from Cloudflare's cache by address, 30 to a request as the Free plan allows. No ASP.NET Core, so a worker can use it.

IHumanCheck TurnstileCheck CloudflareCache AddTurnstile

Webling.Aspire
Aspire 13.6

Container Apps behind Cloudflare

Ingress that admits only Cloudflare's IPv4 ranges, probes that pass AllowedHosts, an uploaded Origin CA certificate, and a deployment step that runs your migrations job before any app rolls out.

UseCloudflareIngress EnsureRangesCurrentAsync RunJobBeforeApps

Webling.Tailwind
Build only

Tailwind without Node

The pinned Tailwind v4 standalone CLI, downloaded once per user and checked against its SHA-256, compiles your stylesheet before each build that changed a style or a component. Nothing ships with your app.

TailwindInput TailwindOutput TailwindSource

Webling.Images
SkiaSharp

Image uploads

Formats recognized by their first bytes, HEIC refused with a reason, uploads cut off at a size limit, and photos re-encoded upright, in sRGB and without metadata, as WebP in several sizes or JPEG on white.

ImageSignature UploadStream ImageEncoder

Every package grew out of Adjudeca and is used by it. Nothing here is a framework: each piece is a few hundred lines you could read in one sitting.

A job that keeps failing

Delivery is at least once, so handlers are idempotent. A failure goes back to pending with a growing delay and jitter, so jobs that failed together don't retry together. After the fifth attempt the job stays failed with its last error, for 30 days.

  1. attempt 1fails30 s + ≤20%

    Back to pending.

  2. attempt 2fails1 min + ≤20%

    Doubles each time.

  3. attempt 3fails2 min + ≤20%

    Capped at 1 hour.

  4. attempt 4fails4 min + ≤20%

    Any replica may take it.

  5. attempt 5failsfailed

    Kept with its error.

pending→running→done · lease expired → claimed again · host stopping → pending, attempt given back · last attempt → failed

Only Cloudflare gets in. Only your zone gets past.

Allowing Cloudflare's IP ranges at the ingress keeps the internet out, but any Cloudflare customer's zone can still reach your origin. A secret that only your zone's Transform Rule sends closes that gap, and the checks run before anything else in the pipeline.

Health probes from inside the environment still get through; the same paths requested through Cloudflare answer 404.

ingress Cloudflare's IPv4 ranges only
Bundled with the package and checked against cloudflare.com every week, and again when you deploy.
app X-Origin-Secret or 403
Compared in constant time, then removed from the request so nothing logs it. Outside Development the app refuses to start without a secret of at least 32 characters.
app Client IP after the check
CF-Connecting-IP becomes the remote address only on requests that passed it, so rate limits see the visitor, never a spoofed header.
edge Cached, then purged
Public pages are cached for five minutes and served stale for ten more; the worker purges a page when its content changes. A visitor with a personal cookie always gets a private answer.

Install

One tag versions all six packages, so keep them on the same version. Webling is 0.x: any minor version may change public types.

NuGet

.NET 10

dotnet add package Webling.Jobs
dotnet add package Webling.AspNetCore
dotnet add package Webling.Cloudflare
dotnet add package Webling.Aspire     # AppHost
dotnet add package Webling.Images

<!-- build only -->
<PackageReference Include="Webling.Tailwind" Version="0.1.0"
                  PrivateAssets="all" />

What it assumes

The stack it was built for; parts work alone

  • PostgreSQL through EF Core for the job table (the claim is PostgreSQL SQL).
  • Cloudflare in front, with a Transform Rule that sends the origin secret.
  • Azure Container Apps, published by Aspire from CI, for the AppHost pieces.
  • Tailwind CSS v4; the standalone CLI exists for macOS, Linux (glibc and musl) and Windows x64.

No support. Built for my own projects and shared as is: issues and pull requests aren't monitored, and any release may change or break things.