The short version: Fair Lawn Apps runs on Cloudflare's edge network with HTTPS everywhere, uses passkeys instead of passwords, stores minimal data with automatic expiration, and never handles sensitive personal information.
Overview
Fair Lawn Apps is a community project that makes public information from fairlawn.org and other sources more accessible. It handles gym schedules, library programs, pool hours, local alerts, Farmers Market status, and theater events.
The amount of data involved is deliberately small: push notification subscriptions, anonymous passkey credentials, and user preferences. There are no user accounts with names or emails, no payment processing, and no sensitive personal records. That limited scope is itself a security advantage โ there is very little to protect, and every layer described below exists to protect it.
Data in transit
All traffic between your browser and Fair Lawn Apps is encrypted with HTTPS (TLS 1.2+). There are no HTTP fallbacks.
- App pages: served via Cloudflare Pages with automatic TLS certificate management
- API requests: handled by a Cloudflare Worker at
flcc-push.trueto.workers.dev, also HTTPS-only - Push notifications: delivered using the Web Push protocol with end-to-end encryption (RFC 8291) via the
@block65/webcrypto-web-pushlibrary - Scraper traffic: data is fetched from public websites over HTTPS and committed to the repository as static JSON files
Data at rest
Cloudflare Workers KV
Push subscriptions, notification preferences, occupancy reports, alert watches, and feedback are stored in Cloudflare Workers KV.
- Subscription endpoints are stored as SHA-256 hashes โ the raw endpoint URL is stored in the value, not the key
- Feedback entries expire after 90 days (automatic TTL)
- Alert watch records expire after 30 days
- Occupancy check-in data expires after 2 hours
- Rate-limiting keys expire after their cooldown period
Cloudflare D1
The D1 SQLite database stores anonymous passkey accounts and sessions, managed through better-auth with the Drizzle ORM.
- No email addresses, names, or personal identifiers โ accounts are fully anonymous
- Sessions expire after 1 year of inactivity and extend on daily use
- Five tables: user, session, account, verification, passkey โ all containing only system-generated identifiers
No PII beyond optional feedback email
The only personal information Fair Lawn Apps may hold is an email address if a user voluntarily provides one in the feedback form. That email is stored alongside the feedback message (with a 90-day TTL) and is used only if a follow-up is needed.
Authentication
Fair Lawn Apps uses passkeys (WebAuthn/FIDO2) for cross-device sync. This is entirely optional โ the apps work without any sign-in at all.
- No passwords to remember or reuse: authentication is passkey-only. Users tap their device biometric or security key.
- Private key never leaves the device: the passkey credential stays in the device's secure enclave (Touch ID, Face ID, Windows Hello, etc.). Only the public key and a credential ID are stored server-side.
- Anonymous accounts: when a passkey is first set up, an anonymous account is created with a random UUID as the internal email and
crypto.randomUUID()as the password (122 bits of entropy). Users never see or type this password. - SHA-256 for internal password hashing: because passwords are random UUIDs (immune to dictionary attacks regardless of hash speed), SHA-256 is used instead of scrypt/bcrypt to stay within Cloudflare Workers' free-tier CPU limits (10 ms per request). This trade-off is safe here โ the password exists only as a framework requirement and is never user-controlled.
- Cross-origin cookies: the app domain (
fairlawn.paragbaxi.com) and the worker (flcc-push.trueto.workers.dev) are different origins, so session cookies are set withSameSite=None; Secure. A Bearer token fallback handles Safari's Intelligent Tracking Prevention (ITP).
Secret management
All secrets are stored as Cloudflare Workers secrets (encrypted at rest, injected at runtime). None appear in source code or environment variables checked into the repository.
- VAPID keys: identify the push notification sender โ public key is embedded in the app, private key is a worker secret
- NOTIFY_API_KEY: protects the
/notifyadmin endpoint that triggers fan-out push delivery - APP_SECRET: signs session cookies (HMAC-SHA256)
- TURNSTILE_SECRET_KEY: validates Cloudflare Turnstile challenges on the feedback form
- GitHub App private key: used to mint short-lived installation tokens for creating GitHub issues from feedback โ tokens expire in 10 minutes
Bot protection
The feedback form is protected by Cloudflare Turnstile, a privacy-preserving CAPTCHA alternative. Turnstile verifies that submissions come from a real browser without fingerprinting users or showing image puzzles.
Push notifications
Push delivery follows the Web Push protocol with VAPID authentication (RFC 8292).
- Notification payloads are encrypted end-to-end using the subscriber's public key โ the push service (Google, Apple, Mozilla) cannot read the message content
- Encryption is handled by
@block65/webcrypto-web-push, which uses the Web Crypto API (no OpenSSL dependencies) - Subscription endpoints are stored by their SHA-256 hash โ listing KV keys does not reveal the actual push endpoint URLs
- Expired or unsubscribed endpoints (HTTP 410/404) are automatically cleaned up during each fan-out delivery
Rate limiting
Rate limits protect against abuse while preserving privacy.
- Occupancy check-ins: one report per IP per 15 minutes โ the IP is SHA-256 hashed before storage, so the raw IP address is never persisted
- Feedback submissions: 10 messages per IP per 24 hours โ same SHA-256 hashing, with only a truncated hash prefix stored alongside the feedback
- Rate-limiting keys expire automatically via KV TTLs
OWASP Top 10 self-assessment
A brief status against the OWASP Top 10 (2021) categories.
Admin endpoints (/notify, /stats) require an API key via the X-Api-Key header. CORS is configured per-endpoint: credentialed auth routes restrict the origin to the app domain; public data routes use Access-Control-Allow-Origin: * where appropriate (e.g., subscription PATCH uses endpoint-as-auth, no cookies needed).
TLS everywhere. Session cookies signed with HMAC-SHA256. Push payloads encrypted per RFC 8291. Internal passwords are random UUIDs โ SHA-256 is appropriate for that entropy level.
No raw SQL โ the D1 database is accessed exclusively through the Drizzle ORM with parameterized queries. User input (feedback messages, notification preferences) is validated and typed before use.
Minimal attack surface by design: anonymous accounts, no PII collection, automatic data expiration, and passkey-only authentication.
TLS is managed by Cloudflare (automatic certificate renewal, modern cipher suites). The worker runs on Cloudflare's hardened V8 isolate runtime. No server configuration files to misconfigure.
Dependencies are monitored via npm audit. Known CVEs in transitive dependencies (e.g., rollup, hono, lodash) are addressed via package.json overrides to force patched versions.
Passkey-only authentication eliminates password reuse, credential stuffing, and phishing. No password reset flows. No security questions. Session tokens are HMAC-signed and transmitted over HTTPS only.
JSON.parse is used only on data from trusted sources (KV store, authenticated API responses). Svelte auto-escapes all rendered content โ no innerHTML or {@html} in user-facing components.
Worker errors and push delivery failures are logged via Cloudflare Workers analytics (console.error/console.warn). No sensitive data (tokens, keys, endpoints) appears in logs. Umami analytics tracks page views without cookies or personal data.
The worker makes outbound requests only to fixed, hardcoded URLs: push endpoints (controlled by the browser, validated by the push service), the GitHub API, and Cloudflare's Turnstile verification endpoint. No user-controlled URLs are fetched server-side.
Incident response
In the event of a security incident:
- Affected KV data (subscriptions, feedback) can be purged immediately via the Cloudflare dashboard or Wrangler CLI
- D1 sessions can be revoked by rotating the
APP_SECRETworker secret, which invalidates all existing session cookies - API keys and VAPID keys can be rotated independently without downtime
New Jersey reporting requirements
Under New Jersey law (S297), entities holding personal data of NJ residents must report qualifying security incidents to the NJ Cybersecurity and Communications Integration Cell (NJCCIC) within 72 hours of discovery.
Because Fair Lawn Apps collects almost no personal data (no names, no payment information, and only optional email addresses in feedback), the scope of any reportable incident is inherently limited. That said, the 72-hour reporting obligation is taken seriously.
Contact
To report a security concern, use the feedback form. For anything urgent, include "SECURITY" in the message โ those are triaged immediately.
Third-party services
- Cloudflare โ Pages (static hosting), Workers (API), KV (key-value storage), D1 (SQLite database), Turnstile (bot protection). Cloudflare provides DDoS protection, TLS termination, and edge caching.
- GitHub โ source code hosting, CI/CD via GitHub Actions, issue creation from feedback form via a GitHub App with minimal scoped permissions (issues only).
- Umami โ privacy-focused, cookie-free analytics. No personal data collected. GDPR and CCPA compliant by design.
No data is shared with advertisers, data brokers, or other third parties. The services above are infrastructure providers, not data consumers.
Open source
Fair Lawn Apps is open source. The full codebase โ including the worker, scrapers, and all client apps โ is available on GitHub for anyone to inspect.
Last updated: March 16, 2026
See also: Privacy Policy ยท Terms of Service