My astro website
  • TypeScript 48.3%
  • Astro 31.5%
  • CSS 20.1%
  • JavaScript 0.1%
Find a file
lutvzn bbb1bf2f83
All checks were successful
Validate and deploy / Validate (push) Successful in 1m42s
Validate and deploy / Deploy both Workers concurrently (push) Successful in 1m13s
Fixed formatting error
2026-09-22 23:25:35 -03:00
.forgejo/workflows Renamed lctcx to lct 2026-09-18 17:11:04 -03:00
apps Fixed formatting error 2026-09-22 23:25:35 -03:00
packages Fixed contact form 2026-08-25 11:08:51 -03:00
tests Renamed lctcx to lct 2026-09-18 17:11:04 -03:00
.editorconfig Moving to GitHub 2026-08-15 19:18:44 -03:00
.gitignore Moving to GitHub 2026-08-15 19:18:44 -03:00
AGENTS.md Renamed lctcx to lct 2026-09-18 17:11:04 -03:00
biome.json Updated bun and astro and made design changes 2026-08-27 10:42:56 -03:00
bun.lock Renamed lctcx to lct 2026-09-18 17:11:04 -03:00
bunfig.toml Moving to GitHub 2026-08-15 19:18:44 -03:00
package.json Renamed lctcx to lct 2026-09-18 17:11:04 -03:00
README.md Fixed formatting error 2026-09-22 23:25:35 -03:00
spec.md Renamed lctcx to lct 2026-09-18 17:11:04 -03:00
tsconfig.json Moving to GitHub 2026-08-15 19:18:44 -03:00
tsconfig.tests.json Moving to GitHub 2026-08-15 19:18:44 -03:00

lutvzn.com and lct.st

Bun workspace for two independent Astro applications deployed as Cloudflare Workers:

  • lutvzn.com: professional site, blog, and protected contact endpoint
  • lct.st: short links, Cloudflare Access-protected administration, and self-hosted service status

This repository implements all 16 phases of spec.md. The lutvzn.com site includes its local Markdown blog and protected contact form. Most public pages and all blog articles are prerendered, including /contact; only POST /api/contact runs in the Worker for that flow. The independent lct.st application includes D1-backed redirects, Cloudflare Access authentication, and its protected short-link manager, protected monitored-service configuration, five-minute scheduled checks, and public status experience.

Architecture

Each app owns its Astro and Wrangler configuration and deploys to a separate Worker. Astro is configured for server output because both apps have request handlers, while routes that do not need runtime work opt into prerendering so Cloudflare Static Assets can serve them directly.

Shared packages contain only implementations used by both applications:

.
├── apps/
│   ├── lutvzn/       # Worker for lutvzn.com
│   └── lct/          # Worker for lct.st
├── packages/
│   ├── ui/           # Future genuinely shared UI primitives
│   └── utils/        # Shared HTTP limits, origin checks, and security headers
├── .forgejo/workflows/ # Forgejo validation and concurrent production deployment
├── tests/             # Focused Bun unit and security-regression tests
├── biome.json        # Workspace formatting and linting policy
├── bunfig.toml        # Bun test discovery and timeout configuration
├── package.json      # Bun workspaces and repository scripts
├── tsconfig.json     # Strict shared TypeScript defaults
└── README.md

No monorepo orchestrator or client-side framework is used.

Prerequisites

  • Git and access to this private repository
  • Bun 1.3.12, matching the pinned packageManager version
  • A Cloudflare account for remote resources and deployment
  • A Cloudflare Zero Trust organization and identity provider for the lct.st administrator
  • Active lutvzn.com and lct.st zones in that account before attaching custom domains
  • A NameCrane CraneMail mailbox or app password for contact@lutvzn.com

Cloudflare access is not required to install, typecheck, test, build, or use isolated local D1.

Fresh clone and validation

Clone the repository, enter its root, and install exactly what is recorded in bun.lock:

git clone <private-repository-url> web
cd web
bun install --frozen-lockfile

Run the complete local validation sequence from the repository root:

bun lint
bun typecheck
bun test
bun run build
bunx wrangler deploy --dry-run --config apps/lutvzn/dist/server/wrangler.json
bunx wrangler deploy --dry-run --config apps/lct/dist/server/wrangler.json

For ordinary dependency installation after the first clone, bun install is available; use the frozen form in CI and whenever reproducibility matters. bun typecheck regenerates ignored Cloudflare binding declarations in parallel, then checks packages, apps, and tests in parallel.

Formatting and linting use Biome, including its experimental full Astro support:

bun lint       # check formatting, imports, and lint rules
bun format     # apply formatting, import organization, and safe fixes

Intentional control-character validation expressions and narrowly scoped CSS overrides are documented exceptions to the recommended rules in biome.json.

Continuous integration

.forgejo/workflows/ci.yml is the repository's only CI workflow. It runs for pull requests and pushes to master, installs the pinned Bun version with the frozen lockfile, runs Biome, typechecks, executes the focused test suite, builds both applications, and validates both generated Worker bundles with Wrangler dry runs. Pull requests stop after validation and never receive Cloudflare credentials.

After a successful master validation, one production job rebuilds both applications and launches the two independent Wrangler deployment processes concurrently. The job waits for both processes, so one failure does not stop the other deployment from finishing, and fails if either process fails. Running both commands inside one job guarantees that they overlap even when the Forgejo runner can execute only one job at a time. The lutvzn.com runtime secrets are passed through a permission-restricted temporary file and removed from the process environment before either Wrangler process starts.

Forgejo runner Docker networking

The workflow requires a runner label matching ubuntu-latest, configured with a Linux Docker image that contains Node.js 24 or later. The workflow explicitly uses the node:24-bookworm job image so Wrangler and its dependencies do not depend on the runner's older default image. The runner creates an isolated Docker network before the first step. If a run fails before checkout with could not find free subnet from subnet pools, the workflow has not executed yet: Docker on the runner has exhausted its automatically allocated bridge-network subnets, commonly because old job networks remain or the daemon's pools are too small.

On the runner host, inspect networks first and remove only unused networks after confirming that no other workloads depend on them:

docker network ls
docker network prune

For a durable fix, configure Docker's /etc/docker/daemon.json with one or more unused private address pools. Choose ranges that do not overlap the host LAN, VPNs, Kubernetes, or existing container networks. Smaller allocations such as /24 provide many more job networks than the default /16 allocations. For example, on a dedicated runner where these ranges are unused:

{
  "default-address-pools": [
    { "base": "172.30.0.0/16", "size": 24 },
    { "base": "172.31.0.0/16", "size": 24 }
  ]
}

Restart Docker and then the Forgejo Runner after changing this file:

sudo systemctl restart docker
sudo systemctl restart forgejo-runner

Changing the Forgejo Runner's container.network setting to bridge is a short-term workaround that avoids allocating a new network per job, but it reduces job isolation and should only be used on a trusted, dedicated runner. Do not add a workflow step for this error: the runner must create the network before any workflow step can run.

Persistent Bun cache with rootless Podman

The Forgejo workflow mounts a runner-local Bun package cache into each node:24-bookworm job. This avoids transferring a compressed actions/cache archive on every run. The following assumes the rootless runner uses UID 5997 and a Quadlet service that mounts /data and the rootless Podman socket; replace the path or UID if the runner differs.

Create the host directory and grant the rootless Podman user access:

install -d -m 0770 /home/lutvzn/.local/share/containers/forgejo-runner/bun-cache
chown 5997:5997 /home/lutvzn/.local/share/containers/forgejo-runner/bun-cache

Add this volume to the Quadlet container unit:

Volume=/home/lutvzn/.local/share/containers/forgejo-runner/bun-cache:/cache/bun:z

In /data/runner-config.yml, allow only that host volume under the existing container: section:

container:
  valid_volumes:
    - /home/lutvzn/.local/share/containers/forgejo-runner/bun-cache

Reload the Quadlet service using the same systemd scope that currently runs it:

systemctl --user daemon-reload
systemctl --user restart forgejo-runner

The workflow's job containers mount the source path at /root/.bun/install/cache. This cache is shared and writable by workflow jobs, so it must contain dependency packages only—not secrets or other trusted runner data.

Tests

The focused Bun suite lives in tests/ and runs with Bun's built-in test runner:

bun test
bun run test:watch

The suite covers slug and reserved-route validation, destination URLs, Cloudflare Access JWT signature and claim validation, contact validation, status calculations, Turnstile response context, origin checks, bounded form bodies, and generated security headers. Test files have their own strict tsconfig.tests.json, which is included in the root bun typecheck command. No Workers-specific test utility is currently needed because these boundaries are exercised through plain modules and standard Web APIs; add one only when a future test requires the Workers runtime itself.

Local development

Astro 7's Cloudflare adapter uses the local Workers runtime during development, so bindings from each app's wrangler.jsonc are available with production-like behavior.

For lutvzn, copy the public and runtime examples before testing the contact form:

cp apps/lutvzn/.env.example apps/lutvzn/.env
cp apps/lutvzn/.dev.vars.example apps/lutvzn/.dev.vars

For lct, apply both committed migrations to isolated local D1:

bun run db:migrate:lct:local

PowerShell users can replace cp with Copy-Item. The local Wrangler configuration supplies an explicit development identity only for loopback requests, so Cloudflare Access is not required for local development.

Start either app from the repository root:

bun dev:lutvzn
bun dev:lct

The commands run separately and both default to port 4321. Pass Astro options after -- when a different host or port is needed, for example:

bun dev:lct -- --port 4322

After a build, run a production-runtime preview with:

bun preview:lutvzn
bun preview:lct

Blog content

Posts live in apps/lutvzn/src/content/blog as Markdown. Frontmatter is validated by apps/lutvzn/src/content.config.ts:

---
title: A concise article title
description: A useful description for listings, search, and feeds.
publishedAt: 2026-08-15
updatedAt: 2026-08-16 # optional; cannot precede publishedAt
draft: false
tags:
  - Product
  - Engineering
---

The filename becomes the URL identifier, so clear-systems.md is published at /blog/clear-systems/. Drafts appear during local development with a draft badge and noindex, but are excluded from production article routes, listings, RSS, and sitemap output.

Generated discovery endpoints:

  • /rss.xml
  • /sitemap-index.xml
  • /robots.txt

Contact form

POST /api/contact accepts name, email, subject, and message form fields. The endpoint validates and bounds every field on the server, checks a decoy field, applies a four-attempt-per-minute Cloudflare rate limit keyed by a hash of the connecting IP, verifies Turnstile, and then hands a plain-text message to a small email-provider interface. The configured site address is always the sender; the visitor address is used only as Reply-To.

The contact page and its success/error result variants are prerendered. Non-JavaScript submissions redirect to one of those static result pages, while enhanced submissions display the same response inline. Static-asset routing sends only /api/contact through the Worker and serves the generated 404 page directly for all other unmatched lutvzn.com paths.

The default provider sends through the existing NameCrane CraneMail mailbox over authenticated SMTP with implicit TLS. The Worker accepts only ports 465 and 2465; use the exact hostname and port shown in CraneMail's Connection Guide. Resend remains available as a dependency-free HTTP fallback by setting EMAIL_PROVIDER to resend in apps/lutvzn/wrangler.jsonc and configuring RESEND_API_KEY.

For local contact-form development, copy both examples and restart the dev server after changing either file:

Copy-Item apps/lutvzn/.env.example apps/lutvzn/.env
Copy-Item apps/lutvzn/.dev.vars.example apps/lutvzn/.dev.vars
bun dev:lutvzn

The examples use Cloudflare's published always-pass Turnstile test keys. Never deploy those keys. The SMTP example hostname is intentionally invalid. Replace it and the example credentials only when deliberately testing real NameCrane delivery; local SMTP tests send real messages.

For production:

  1. In the Cloudflare dashboard, create a managed Turnstile widget, allow the exact lutvzn.com hostname, and retain its site key and secret key. Make the public site key available to the Astro build as PUBLIC_TURNSTILE_SITE_KEY; the server-side key becomes TURNSTILE_SECRET_KEY.
  2. In NameCrane, open CraneMail's Connection Guide and copy the implicit-TLS SMTP hostname and port. Use 465 when offered; 2465 is also supported by this Worker. Keep NameCrane's existing MX, SPF, DKIM, and DMARC records in place.
  3. Set SMTP_USERNAME and CONTACT_FROM_EMAIL to contact@lutvzn.com. Set CONTACT_DESTINATION_EMAIL to the inbox that should receive submissions; it may also be contact@lutvzn.com. Use a dedicated app password as SMTP_PASSWORD when mailbox 2FA is enabled.
  4. Store the runtime values as Worker secrets from apps/lutvzn:
bunx wrangler secret put TURNSTILE_SECRET_KEY
bunx wrangler secret put CONTACT_FROM_EMAIL
bunx wrangler secret put CONTACT_DESTINATION_EMAIL
bunx wrangler secret put SMTP_HOST
bunx wrangler secret put SMTP_USERNAME
bunx wrangler secret put SMTP_PASSWORD

Build and deploy with the public site key in the build environment:

$env:PUBLIC_TURNSTILE_SITE_KEY = 'your-production-site-key'
bun deploy:lutvzn

For the Resend fallback, verify the sender domain with Resend, change EMAIL_PROVIDER to resend, and additionally run bunx wrangler secret put RESEND_API_KEY. The public Turnstile site key is not a secret; TURNSTILE_SECRET_KEY, SMTP credentials, provider credentials, and contact addresses must never enter client-side code. The rate-limit namespace ID in wrangler.jsonc must remain a unique positive integer within the Cloudflare account if other rate-limit bindings are added.

Cloudflare's Turnstile widget documentation distinguishes the public site key from the private verification key. Its hostname management documentation describes the hostname allowlist. NameCrane's CraneMail quickstart locates the per-account Connection Guide; Cloudflare documents the Worker's outbound TLS socket API.

lct.st application shell

lct.st uses a compact, dark, utility-oriented interface that is intentionally independent from the lutvzn.com design system. Its current routes are:

  • / — prerendered public landing page
  • /status — dynamic, cached public service state and recent history
  • /admin — authenticated, dynamic, noindex administrative overview
  • /admin/links — authenticated short-link manager
  • /admin/services — authenticated monitored-service manager
  • /.well-known/matrix/client — prerendered Matrix client discovery document for https://matrix.lct.st
  • /.well-known/matrix/server — prerendered Matrix federation discovery document for matrix.lct.st:443
  • /:slug — runtime D1 lookup for enabled short links
  • /404.html — general not-found page for routes outside the single-segment slug boundary

The admin routes support managing both short links and monitored-service definitions. Enabled services are checked every five minutes and safely summarized on the public status route. Reserved application routes take precedence over the /:slug resolver.

The initial migration is apps/lct/migrations/0001_create_links.sql. It creates the links table with timestamps, an enabled flag, case-insensitive uniqueness, application-aligned constraints, and indexes for public lookup and administration. The repository isolates prepared D1 statements; the service layer owns normalization, duplicate detection, and validation shared by public resolution and protected admin actions.

Slugs are normalized to lowercase, may contain letters, numbers, and single internal hyphens, and are limited to 64 characters. .well-known, admin, api, status, and 404 are reserved. Destinations are normalized with the URL API, limited to 2,048 characters, restricted to http: and https:, and cannot contain credentials. The resolver validates stored destinations again before redirecting.

Enabled links return a temporary 302 response with Cache-Control: private, no-store. This is deliberate: destinations and enabled state are editable, so clients and intermediary caches must not pin an old target as they could with a permanent redirect. Disabled, missing, malformed, and reserved slugs return 404; binding and query failures return 503. Click analytics are intentionally not recorded, but lookup remains isolated behind the repository/service boundary so analytics can be added later without changing routing rules.

Local D1 setup

The committed apps/lct/wrangler.local.jsonc is local-only and supplies a DB binding without pretending to be a production database. Wrangler persists its ignored local state in .wrangler. From the repository root:

bun run db:migrate:lct:local
bun dev:lct

Open http://localhost:4321/admin, choose Links, and create the first link. The manager applies the same slug and destination rules as the public resolver. Visit the displayed public URL to test it; disabled links return 404.

Production D1 setup

The committed apps/lct/wrangler.jsonc retains the existing production database UUID so links, services, and health history survive the internal rename. Do not create a replacement database or change that UUID during migration. Authenticate and apply pending migrations from apps/lct:

bunx wrangler login
bunx wrangler d1 migrations apply lct --remote

Keep binding as DB, database_name as lct, and migrations_dir as migrations. The database UUID is a resource identifier rather than a credential and belongs in Wrangler configuration. For a separate fresh installation with no existing database, run bunx wrangler d1 create lct, replace the committed UUID with the returned UUID, and then apply the migrations. Once deployed with Cloudflare Access configured, create and manage production links at https://lct.st/admin/links; direct SQL inserts are unnecessary.

Before and after applying migrations, inspect their remote state from apps/lct with:

bunx wrangler d1 migrations list lct --remote

Administrator authentication

Cloudflare Access protects /admin, /admin/*, /api/admin, and /api/admin/*. Public short-link redirects and /status remain unauthenticated. Create one Self-hosted Access application and add all four path entries because a trailing wildcard does not cover its parent path. Access applications deny by default; attach an Allow policy scoped to the administrator's exact IdP email (or a verified IdP group), and require MFA in the IdP or Access policy. Do not configure a Bypass policy for these paths.

The Worker also validates Cf-Access-Jwt-Assertion using Cloudflare's rotating remote JWKS. It requires an RS256 signature, the exact team-domain issuer, the application's audience, expiry, subject, and email claims. This second check keeps the workers.dev hostname and accidental Access policy gaps fail-closed. Copy the Application Audience (AUD) Tag from the Access application's additional settings and set both account-specific values from apps/lct:

bunx wrangler secret put ACCESS_TEAM_DOMAIN
# Enter: https://<your-team-name>.cloudflareaccess.com
bunx wrangler secret put ACCESS_AUD
# Enter the application AUD tag.

These values are identifiers rather than credentials, but storing them as Worker secrets avoids committing account-specific deployment metadata. The Access application session duration controls reauthentication. The admin overview links to /cdn-cgi/access/logout, which clears the Access authorization session. Administrator mutations additionally require same-origin Origin or Referer evidence and same-origin Fetch Metadata.

Local requests to localhost, 127.0.0.1, or [::1] use ACCESS_DEV_EMAIL from wrangler.local.jsonc. The bypass is rejected for every other hostname and is not present in the production Wrangler configuration.

Uptime data and service management

Migration apps/lct/migrations/0002_create_uptime.sql adds:

  • services for protected monitor configuration
  • health_checks for raw five-minute results
  • health_hourly and health_daily for bounded long-term aggregates

Each service stores a display name, private check URL, optional public URL, enabled state, expected HTTP status, and a timeout constrained to 1–30 seconds. Only authenticated admin and scheduled-check code uses the full service record containing check_url. The public status repository uses an explicit safe projection containing only the display name and validated optional public URL.

Apply the migration locally, start the app, and open /admin/services:

bun run db:migrate:lct:local
bun dev:lct

To create the first monitored service:

  1. Open http://localhost:4321/admin/services.
  2. Enter a display name and a complete private http: or https: health-check URL. This URL is stored in D1 but is never selected by the public status query.
  3. Optionally enter the public page visitors may open from /status.
  4. Set the exact successful HTTP status, normally 200, and a timeout from 1,000 to 30,000 ms.
  5. Leave the service enabled, submit it, and invoke the local scheduled endpoint described below.
  6. Open http://localhost:4321/status and confirm the service state and history. Repeat these steps at https://lct.st/admin/services after production deployment.

Before deploying this phase, apply the same committed migration to the existing production D1 database from apps/lct:

bunx wrangler d1 migrations apply lct --remote

The retention design is executed by the scheduled handler without extra infrastructure:

  • Keep raw health_checks for approximately seven days.
  • Roll older raw rows into UTC hourly buckets in health_hourly, then delete the aggregated raw rows.
  • Keep hourly buckets through approximately 90 days.
  • Roll older hourly buckets into UTC daily buckets in health_daily, then delete the aggregated hourly rows.
  • Retain daily aggregates for long-term history; a future storage policy may set a final horizon if actual usage warrants one.

Retention aggregation runs during the first scheduled five-minute window after 00:00 UTC each day; the remaining monitoring cycles only check services and record their raw results.

The aggregate tables retain counts and latency statistics, allowing uptime percentages and useful latency summaries without preserving every five-minute row indefinitely.

Scheduled monitoring

apps/lct/src/worker.ts is a custom Worker entrypoint that delegates HTTP requests to Astro and handles the */5 * * * * UTC Cron Trigger. Each invocation queries enabled services and checks them concurrently. Every service has its own abort timeout, so one slow or failing endpoint cannot stop the others.

A check succeeds only when the returned status exactly matches expected_status. The raw row stores the observed HTTP status and latency. Connection failures, timeouts, and unexpected statuses are stored respectively as network, timeout, and unexpected_status; URLs and exception messages are not written to logs. D1 write failures are reported only after all enabled services have been attempted, making the cron invocation visibly fail without sacrificing isolation.

To invoke the scheduled handler locally, start lct and call Cloudflare's local scheduled endpoint from another PowerShell window:

bun dev:lct
Invoke-WebRequest "http://localhost:4321/cdn-cgi/handler/scheduled?cron=*/5+*+*+*+*&format=json"

Inspect recent local results with:

bun run db:query:lct:local -- --command "SELECT service_id, checked_at, succeeded, http_status, latency_ms, failure_reason FROM health_checks ORDER BY checked_at DESC LIMIT 20;"

The cron trigger is registered or updated when bun deploy:lct deploys the Worker. The production D1 migration and Cloudflare Access metadata must be configured before opening the admin surface.

Public status

/status dynamically reads at most 100 enabled services, the latest 24 raw checks per service, and 24-hour success counts. Production Workers Caching serves fresh status responses for five minutes without re-running Astro or D1, allows a short stale-while-revalidate window, and separately permits browsers to reuse a response for 60 seconds. Database failures return a generic 503 with no caching. Private admin pages, short-link redirects, and error responses explicitly bypass the Worker cache.

The public query never selects the private check URL. Stored public URLs are also validated again before rendering. Disabled services are omitted entirely.

Status rules are:

  • Operational: the latest check returned the configured expected HTTP status.
  • Degraded: the latest check received an HTTP response with a different status.
  • Offline: the latest check ended in a network failure or timeout.
  • Unknown: no check exists or the latest check is more than 15 minutes old.

Overall state reflects the most severe enabled service in the order offline, degraded, unknown, then operational. The 24-hour uptime percentage is successful raw checks divided by all raw checks in that window. Each service shows up to 24 accessible history segments ordered oldest to newest.

Cloudflare configuration

The core Worker configuration is in:

  • apps/lutvzn/wrangler.jsonc
  • apps/lct/wrangler.jsonc

Both configs use Astro's Cloudflare integration, a pinned compatibility date, static asset serving from dist, and Worker observability. lutvzn uses a static 404 fallback with selective Worker-first routing for /api/contact. The production lct configuration enables Workers Caching for explicitly public responses and uses a custom entrypoint to combine Astro HTTP handling with scheduled events. nodejs_compat is enabled for adapter and dependency compatibility; application code should still prefer Web APIs.

The apps/lct/wrangler.jsonc Worker is named lct and explicitly enables workers_dev. With the account subdomain lutvzn, its deployment URL is:

https://lct.lutvzn.workers.dev

The public custom domain lct.st remains separate from the Worker name. The logical production D1 database name is lct; its existing UUID is retained in Wrangler to preserve the existing data and binding. The database name changes without creating a replacement database.

The Cloudflare adapter currently adds an automatically provisioned SESSION KV binding to its generated deploy configuration. The applications do not rely on it; lct uses Cloudflare Access and verifies the forwarded Access JWT without storing application sessions.

The production DB binding is configured in wrangler.jsonc; local development uses the isolated database in wrangler.local.jsonc. Both configurations expose the five-minute scheduled handler; only a production deployment registers the remote Cron Trigger.

The root typecheck command generates both config-derived Worker binding types in parallel before checking the independent workspace targets in parallel. You can also regenerate the binding types directly after changing either Wrangler file:

bun cf-types

Generated worker-configuration.d.ts files are local build artifacts and are not committed, so bun typecheck always regenerates them and works in a fresh clone and in CI.

Binding and configuration inventory

lutvzn uses the following production configuration:

Name Kind Required Purpose and source
ASSETS Static Assets binding Yes Serves the generated dist assets; declared in wrangler.jsonc.
CONTACT_RATE_LIMITER Rate-limit binding Yes Four contact attempts per key per minute; declared with namespace 1001.
EMAIL_PROVIDER Plain Worker variable Yes smtp by default; change to resend only with the fallback configured.
SMTP_PORT Plain Worker variable SMTP only Implicit-TLS submission port, 465 by default; NameCrane may alternatively specify 2465.
TURNSTILE_SECRET_KEY Worker secret Yes Verifies contact-form tokens server-side.
CONTACT_DESTINATION_EMAIL Worker secret Yes Inbox receiving contact submissions.
CONTACT_FROM_EMAIL Worker secret Yes Authenticated sender, normally contact@lutvzn.com.
SMTP_HOST Worker secret SMTP only Exact NameCrane hostname from CraneMail's Connection Guide.
SMTP_USERNAME Worker secret SMTP only NameCrane mailbox login, normally contact@lutvzn.com.
SMTP_PASSWORD Worker secret SMTP only Dedicated mailbox password or app password.
RESEND_API_KEY Worker secret Resend only Authorizes the fallback HTTP provider.
PUBLIC_TURNSTILE_SITE_KEY Public build variable Yes for the form Embedded in generated contact-page HTML; set in .env locally or as the Forgejo Actions repository variable for master-branch deployments.

lct uses:

Name Kind Required Purpose and source
ASSETS Static Assets binding Yes Serves generated assets; declared in both Wrangler files.
DB D1 binding Yes lct-local locally and the real lct database remotely.
ACCESS_TEAM_DOMAIN Worker secret or variable Production Exact https://<team>.cloudflareaccess.com JWT issuer and JWKS origin.
ACCESS_AUD Worker secret or variable Production Audience tag of the Access application protecting all admin paths.
ACCESS_DEV_EMAIL Local Wrangler variable Local only Supplies the loopback-only development identity; never configured in production.
*/5 * * * * Cron Trigger Yes for monitoring Invokes the scheduled handler every five minutes in UTC.

Astro's adapter also adds a SESSION KV binding to generated deployment configuration. Neither app uses it. Binding names are application contracts: if a Wrangler name changes, update the code and run bun cf-types before deployment. Rate-limit namespace IDs must be unique within the Cloudflare account unless two bindings are deliberately meant to share counters.

Security model

Both Workers set the same baseline browser protections on dynamic responses through Astro middleware and on Cloudflare-served static responses through each app's public/_headers file. These include framing and MIME-sniffing protection, a restricted referrer policy, a restrictive permissions policy, and one-year HSTS. Astro generates a resource-specific, hash-based Content Security Policy for prerendered documents, while middleware hashes scripts in dynamic HTML and emits the equivalent response header. Neither path requires unsafe-inline. lutvzn allows Turnstile scripts, frames, and connections only from https://challenges.cloudflare.com; lct allows only same-origin application resources.

Contact and administrator form bodies are read with an actual streamed byte limit, including requests without a trustworthy Content-Length. Browser mutations require same-origin Fetch Metadata plus an Origin or Referer. Production Turnstile responses must carry the contact action and match the request hostname, and the server never returns or logs the submitted token. The documented always-pass test secret accepts only a response Cloudflare marks as produced with a testing key, because those fixed test responses do not preserve the widget's action or local hostname. Never deploy that test secret. Cloudflare Access policy enforcement and JWT verification, contact throttling, prepared D1 statements, allowlisted redirects, and safe public status projections provide the remaining abuse and disclosure boundaries.

Origin validation, body limits, headers, and error behavior are unchanged during local Workers development. Access itself is replaced by a loopback-only development identity. The local CSP permits inline styles only on loopback when ACCESS_DEV_EMAIL is present because Vite injects CSS through development-only <style> elements; deployed hostnames retain the strict production style-src. HSTS is sent locally too, but browsers only act on it over HTTPS. Astro's generated hash CSP is intentionally active in builds and preview, but Astro does not support that feature in its Vite-powered dev mode; use a production build plus the app's preview command for CSP verification. The committed Turnstile test keys and isolated local D1 binding are the other intentional local environment differences; never deploy the test keys or .dev.vars files.

Local secrets

Copy the examples for the application you are working on:

cp apps/lutvzn/.dev.vars.example apps/lutvzn/.dev.vars
cp apps/lct/.dev.vars.example apps/lct/.dev.vars

On PowerShell:

Copy-Item apps/lutvzn/.dev.vars.example apps/lutvzn/.dev.vars

Implemented lutvzn secrets and variables:

  • TURNSTILE_SECRET_KEY
  • CONTACT_DESTINATION_EMAIL
  • CONTACT_FROM_EMAIL
  • SMTP_HOST
  • SMTP_USERNAME
  • SMTP_PASSWORD
  • PUBLIC_TURNSTILE_SITE_KEY (public build-time variable in .env locally or a Forgejo Actions repository variable, not a secret)
  • EMAIL_PROVIDER (non-secret Wrangler variable; defaults to smtp)
  • SMTP_PORT (non-secret Wrangler variable; defaults to implicit-TLS port 465)
  • RESEND_API_KEY when Resend is selected

Implemented lct production metadata:

  • ACCESS_TEAM_DOMAIN
  • ACCESS_AUD

Never commit .dev.vars, .env files, API credentials, or private monitored-service URLs. A D1 database UUID is a non-secret resource identifier and is intentionally committed in the production Wrangler configuration. Store production runtime secrets per Worker with wrangler secret put.

Production deployment

Forgejo pushes to master deploy both Workers automatically after the validation job succeeds. The workflow starts both Wrangler processes concurrently and waits for both to finish, so a failure in one deployment does not prevent the other from completing. Pull requests remain validation-only. Manual deployment commands are still available below. Run the named scripts with bun run deploy:lutvzn or bun run deploy:lct from the repository root. bun deploy is reserved by Bun and does not run a package script; choose the Worker you want to deploy explicitly.

Forgejo deployment setup

Add these to the repository's Settings > Actions > Secrets:

  • CLOUDFLARE_API_TOKEN — a custom Cloudflare API token with permission to edit Workers, scoped to the account that owns both Workers.
  • CLOUDFLARE_ACCOUNT_ID — the Cloudflare account ID.
  • TURNSTILE_SECRET_KEY — the production Turnstile widget secret.
  • CONTACT_FROM_EMAIL — contact@lutvzn.com.
  • CONTACT_DESTINATION_EMAIL — the inbox that receives contact submissions.
  • SMTP_HOST — the exact implicit-TLS hostname from NameCrane's Connection Guide.
  • SMTP_USERNAME — normally contact@lutvzn.com.
  • SMTP_PASSWORD — the dedicated NameCrane mailbox or app password.

Add this under Settings > Actions > Variables:

  • PUBLIC_TURNSTILE_SITE_KEY — the production Turnstile public site key used while building lutvzn.com. This is public configuration, not a secret.

The workflow uses the pinned Bun and Wrangler versions from the repository lockfile. The trusted master deployment writes the six lutvzn runtime values to a permission-restricted temporary JSON file, unsets those values from the deployment shell, passes the file only to the lutvzn.com wrangler deploy process, and removes it on exit. Wrangler applies those secrets to the new Worker version in the same deployment; they are not exposed to checkout, dependency installation, validation, either build step, or the lct.st Wrangler process. The workflow does not provision Cloudflare resources. Configure the lct ACCESS_* secrets in Cloudflare as described below. Protect the master branch and limit who can push to it because a successful push can deploy both Workers.

1. Authenticate and run the preflight

From the repository root:

bunx wrangler login
bunx wrangler whoami
bun install --frozen-lockfile
bun lint
bun typecheck
bun test
bun run build
bunx wrangler deploy --dry-run --config apps/lutvzn/dist/server/wrangler.json
bunx wrangler deploy --dry-run --config apps/lct/dist/server/wrangler.json

The dry runs package generated Worker output but neither authenticate nor change Cloudflare state.

2. Prepare production resources

Before the first deployment:

  1. Configure the lct D1 database name and retain the existing UUID in apps/lct/wrangler.jsonc, then apply all remote migrations as documented above.
  2. When replacing an already deployed Worker, disable its existing five-minute Cron Trigger before deploying lct. This prevents both Workers from writing duplicate health checks to the shared D1 database during cutover.
  3. Create the production Turnstile widget and prepare its public and secret keys.
  4. Copy the NameCrane SMTP server from CraneMail's Connection Guide and prepare the contact@lutvzn.com mailbox or app password, or configure Resend.
  5. Create the Cloudflare Access application and administrator Allow policy described above, then copy its team domain and audience tag.
  6. Confirm rate-limit namespace ID 1001 does not collide with unrelated bindings in the account.

3. Create the Workers and set secrets

Run an initial deployment to create each Worker at its temporary workers.dev address. Do not attach the public domains until all secrets are present.

PowerShell:

$env:PUBLIC_TURNSTILE_SITE_KEY = 'your-production-site-key'
bun deploy:lutvzn
bun deploy:lct

Push-Location apps/lutvzn
bunx wrangler secret put TURNSTILE_SECRET_KEY
bunx wrangler secret put CONTACT_FROM_EMAIL
bunx wrangler secret put CONTACT_DESTINATION_EMAIL
bunx wrangler secret put SMTP_HOST
bunx wrangler secret put SMTP_USERNAME
bunx wrangler secret put SMTP_PASSWORD
# Only when EMAIL_PROVIDER is "resend":
# bunx wrangler secret put RESEND_API_KEY
Pop-Location

Push-Location apps/lct
bunx wrangler secret put ACCESS_TEAM_DOMAIN
bunx wrangler secret put ACCESS_AUD
Pop-Location

Bash:

PUBLIC_TURNSTILE_SITE_KEY='your-production-site-key' bun deploy:lutvzn
bun deploy:lct

cd apps/lutvzn
bunx wrangler secret put TURNSTILE_SECRET_KEY
bunx wrangler secret put CONTACT_FROM_EMAIL
bunx wrangler secret put CONTACT_DESTINATION_EMAIL
bunx wrangler secret put SMTP_HOST
bunx wrangler secret put SMTP_USERNAME
bunx wrangler secret put SMTP_PASSWORD
# Only when EMAIL_PROVIDER is "resend":
# bunx wrangler secret put RESEND_API_KEY

cd ../lct
bunx wrangler secret put ACCESS_TEAM_DOMAIN
bunx wrangler secret put ACCESS_AUD
cd ../..

wrangler secret put creates and immediately deploys a new Worker version. Run the normal deploy commands again after all values are set so the final version is built from the intended commit:

$env:PUBLIC_TURNSTILE_SITE_KEY = 'your-production-site-key'
bun deploy:lutvzn
bun deploy:lct

Keep PUBLIC_TURNSTILE_SITE_KEY in the deployment environment: it is compiled into the contact page and is not supplied by wrangler secret put.

4. Attach custom domains and verify Access paths

The repository deliberately leaves routes out of both Wrangler files so cloning it does not claim hostnames in a particular Cloudflare account. After both Workers are configured:

  1. In Cloudflare, open Workers & Pages, select the lutvzn Worker, then choose Settings > Domains & Routes > Add > Custom Domain and enter lutvzn.com.
  2. Repeat for the lct Worker with lct.st.
  3. Resolve any conflicting CNAME before attaching the hostname. Cloudflare creates the Worker DNS record and certificate for a Custom Domain.
  4. Attach only the apex names unless a separate www policy is configured. Canonical URLs in this repository use https://lutvzn.com and https://lct.st; a www hostname should redirect to its apex with a Cloudflare Redirect Rule rather than serving duplicate content.

Renaming the existing Worker creates a new Worker deployment. Set ACCESS_TEAM_DOMAIN and ACCESS_AUD on lct, move the lct.st custom domain to it, and verify that the new Worker reads the same D1 data. After cutover, remove the former Worker whose Cron Trigger was disabled before deployment. The configured D1 name changes to lct, while retaining the existing UUID preserves the binding and data; the Cloudflare dashboard resource may retain its previous display name. 5. Confirm the self-hosted Access application covers the four exact admin path entries documented above. Test with the authorized administrator and an unauthorized identity before using the UI.

For an existing lct.cx deployment, complete these migration steps before removing the old hostname:

  • Edit the existing Access application so its four protected paths use lct.st. Editing the application normally preserves its audience; if a replacement application is created instead, copy its new AUD tag into the Worker's ACCESS_AUD secret and redeploy.
  • Create or migrate the DNS, proxy, and origin configuration for chat.lct.st, git.lct.st, and matrix.lct.st. Update the corresponding Element/Matrix and Forgejo public-hostname, trusted-host, and TLS settings before the landing page sends users to them.
  • If lct.cx remains under your control, keep it attached long enough to redirect requests to the same path and query on lct.st, preserving existing short links. If the old domain is no longer controlled, those links cannot be redirected and should be treated as retired.
  • Review existing D1 service records in /admin/services. Update any private check URLs or public URLs that still point at the old service hostnames after the new origins are ready.

Custom Domains are appropriate because each Worker is the origin for every path. See Cloudflare's Custom Domains guide for current dashboard and Wrangler procedures.

5. Verify production

After DNS and certificates are active:

  1. Open https://lutvzn.com, an article, /rss.xml, and /sitemap-index.xml.
  2. Submit the contact form and confirm delivery, sender, and visitor Reply-To behavior.
  3. Open https://lct.st/admin, complete the Cloudflare Access login, create the first short link under /admin/links, and confirm its public URL returns a 302 to the configured destination.
  4. Create the first monitored service under /admin/services. Wait for the next five-minute UTC trigger and verify it appears at /status.
  5. Request https://lct.st/.well-known/matrix/client and confirm it returns the configured Matrix homeserver as JSON with Access-Control-Allow-Origin: *. Request https://lct.st/.well-known/matrix/server and confirm it returns {"m.server":"matrix.lct.st:443"} as JSON.
  6. From apps/lct, inspect recent remote checks without exposing private URLs:
bunx wrangler d1 execute lct --remote --command "SELECT service_id, checked_at, succeeded, http_status, latency_ms, failure_reason FROM health_checks ORDER BY checked_at DESC LIMIT 20;"

Use bunx wrangler tail lutvzn or bunx wrangler tail lct from the corresponding app directory while diagnosing production failures. Observability is enabled in both Wrangler configurations; application logs intentionally omit submitted Turnstile tokens and private check URLs.

Cloudflare free-tier suitability

The following assessment was verified against Cloudflare documentation on 2026-08-16 and must be rechecked before launch because plan limits can change:

  • Prerendered pages and assets should dominate lutvzn.com; Cloudflare documents Static Asset requests as free and unlimited. Only contact submissions—not contact-page views—invoke the lutvzn Worker. Admin, redirect, and uncached status requests invoke lct; cached status hits avoid Worker execution but remain billable requests under current Workers Caching pricing.
  • The Workers Free plan currently allows 100,000 requests per day, 10 ms CPU per HTTP/Cron invocation, 50 external subrequests per invocation, and five Cron Triggers per account. This repository uses two Workers and one five-minute trigger (288 scheduled invocations per day).
  • D1 Free currently includes 5 million rows read per day, 100,000 rows written per day, and 5 GB total storage. A small personal monitor creates roughly 288 raw check records per enabled service each day, plus retention aggregates; D1 billing metrics can count additional writes for indexes. The indexed, bounded status queries and retention policy are designed for this scale.
  • Keep materially fewer than 50 enabled monitored services on the Free plan: each monitoring cycle performs one outbound check per service, and additional redirects made by a target also consume subrequests. Watch Cron CPU time and D1 row metrics as the service count grows.
  • Turnstile supports Free-plan use; a free widget currently allows up to ten hostnames. The contact form needs only lutvzn.com.
  • Contact delivery uses the existing NameCrane subscription over implicit-TLS SMTP and does not require Cloudflare Email Routing or Workers Paid. NameCrane's current sending limits and account terms apply. Resend has its own independent pricing and limits when selected.
  • Workers Logs is included on Free with a current 200,000-event daily allowance and three-day retention. The five-minute completion log and light personal traffic should remain modest.

Current authoritative references: Workers limits, Static Assets billing, D1 pricing, and Workers Logs. Exceeding a Free-plan limit can reject requests or D1 operations rather than silently preserving service, so use the Cloudflare dashboards and upgrade before sustained traffic approaches a limit.

Deliberate choices and limitations

Deliberate choices:

  • Two Workers preserve independent releases, bindings, domains, and failure boundaries.
  • Local Markdown avoids a CMS; plain Astro and server-rendered forms avoid a client framework.
  • Editable short links use non-cacheable 302 redirects rather than permanent redirects.
  • Cloudflare Access owns administrator identity and session policy; the Worker independently verifies its signed application token instead of maintaining a second session store.
  • D1 prepared repositories separate persistence from validation and public response shapes.
  • NameCrane SMTP is the default, with a small Resend fallback rather than a provider SDK.
  • Custom domains are dashboard-managed while code, bindings, migrations, and Cron configuration remain repository-managed.

Current limitations and optional future work:

  • Administrator membership, MFA, recovery, session revocation, and authentication audit logs depend on the configured Cloudflare Access policy and identity provider rather than application code.
  • Short links do not collect click analytics, expiration dates, or per-link access controls.
  • Monitoring performs one HTTP GET from Cloudflare, validates only the exact status, and has no regional probes, custom request headers, incident records, or outage notifications.
  • The public UI emphasizes recent raw checks and 24-hour uptime; stored hourly/daily aggregates are retained for future longer-range views. Daily aggregates currently have no final expiration.
  • The contact rate-limit binding is intentionally permissive and eventually consistent, so it mitigates abuse but is not a billing or identity control.
  • There is no staging Wrangler environment, database backup workflow, or automated end-to-end Workers-runtime suite. Add these only when operational needs justify them. Automatic production deployment is currently configured only for pushes to master in Forgejo.
  • Domain attachment and production SMTP delivery require dashboard/account work and cannot be proven by the repository's offline CI dry runs.

Command reference

Run all commands from the repository root unless a different directory is stated:

# Install and validate
bun install --frozen-lockfile
bun lint
bun format
bun typecheck
bun test
bun run test:watch
bun run build

# Develop and preview
bun dev:lutvzn
bun dev:lct
bun preview:lutvzn
bun preview:lct

# Cloudflare-derived types and local D1
bun cf-types
bun run db:migrate:lct:local
bun run db:query:lct:local -- --command "SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name;"

# Independent production deployments
bun run deploy:lutvzn
bun run deploy:lct