The short version: Fair Lawn Apps runs on Cloudflare's edge network with HTTPS everywhere, stores minimal data, much of it with automatic expiration, and never asks for sensitive personal information: not where you are, your health, your finances, or a government ID. This website has no accounts or passwords at all; the mobile app added an optional account (Sign in with Apple or Google, via Firebase Authentication) so settings can follow a resident to a new phone.
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, and theater events. Farmers Market status was retired from the apps in August 2026: the market data file is still served at its usual address and a background job still compares it against its previous version, but nothing writes to it any more and no app screen or page displays it.
The amount of data involved is deliberately small, and smaller than it looks. Only a handful of things are written to this project's own storage today, all from mobile app residents who opt in: notification tokens and account records, and, from residents who sign in to use them, reports of broken public infrastructure with their confirmations and photos, and βI'd playβ taps on pickup games. The maintainer can add one more, a block on an account that posts abuse. One more thing is collected but not stored here β the email and name of a resident who signs in, which live with Firebase Authentication; that distinction is set out under Personal information collected below, because it decides what a breach of this project's own storage could and could not expose. Two other routes β gym occupancy check-ins and feedback β are still live on the API and still enforce their own limits and TTLs, but their user interface was deleted with the rest of the website in August 2026 and the mobile app never called either of them, so nothing arrives at either. They are described below as live-but-unused rather than omitted, because a route that is switched on is part of the attack surface whether or not anything calls it. Feedback now reaches the maintainer as ordinary email instead.
There is no password of our own anywhere in this system: sign-in for the optional mobile app account is delegated entirely to Apple and Google via Firebase Authentication, so there are no passwords or credentials for this project to store, transmit, or leak. No payment processing and no sensitive personal records either. 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 - 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
This project stores data server-side in three places. Notification registrations, account records and short-lived operational state live in one Cloudflare Workers KV namespace, described here; reports, photos, blocks and βI'd playβ live in Cloudflare D1 and R2, described next. Most of what is in KV expires on its own; two things do not, and that is deliberate.
Expires automatically:
- Feedback entries β 90 days. The route and its TTL still exist; neither the website nor the mobile app sends to it any more
- Occupancy check-in data β 2 hours; same situation, no caller
- Rate-limiting keys β after their cooldown period (15 minutes for occupancy, 24 hours for feedback)
- Operational state β a 60-second health-check cache, the cached FCM access token, send-idempotency markers (2 hours to 14 days), the cron heartbeat (7 days), and the 2-hour tombstone that stops a deleted account being recreated by a token minted before the deletion
Does not expire, on purpose β two record types, both removable by the resident:
- Mobile app notification registrations, keyed by a one-way hash of the notification token. A notification someone asked for should not stop arriving on a timer. Turning notifications off in the app deletes the record
- Mobile app account records, keyed by Firebase user ID. Settings a resident chose should not silently reset. Settings β Account β Delete account removes it
An earlier version of this page said every key had a TTL. That was never true of those two, and the sentence is gone rather than softened.
Cloudflare D1
The mobile app's reports, βI'd playβ and blocks live in Cloudflare D1, Cloudflare's SQL database. Every row that is about an account is keyed by its Firebase user ID, never by a name or an email.
- Reports and confirmations β the place, picked from a fixed list of public places; what kind of problem it is; the Firebase user ID of whoever filed or confirmed it; and the time. No automatic expiry: a report should not vanish while the thing it is about is still broken. Deleting an account deletes its confirmations and replaces the user ID on its reports with a meaningless one that points at nobody
- Photo records β which report a photo belongs to, where it is stored, the Firebase user ID of the account that uploaded it, and whether it is waiting, approved, or turned down. Deleted with the photo, 90 days after upload, or when the account is deleted
- βI'd playβ β the game, the Firebase user ID, and the time. Deleted when the game ends, or sooner when the resident takes themselves off or deletes their account. A note that an account left a park game that was already on is kept until that game ends
- Blocks β the Firebase user ID of a blocked account, the time, and a short note of the reason. No automatic expiry, and deleting the account does not clear it; only the maintainer can lift it
- Preservation holds β if a photo appears to show the sexual abuse of a child, federal law (18 U.S.C. Β§ 2258A) requires it to be preserved, together with the record of which account uploaded it. That record survives account deletion and is kept for as long as that law requires, which is currently one year
- A test copy β test builds of the app, which only the developer runs, file into a second D1 database with the same tables, so testing never reaches the live reports. The accounts that sign in to a test build are real ones, so that copy is held to every rule above: the same expiry, and deleting an account deletes from it too
- Also stored, and not about anyone: a copy of the fixed list of Fair Lawn places, whether each pickup game's one notification has gone out (deleted when the game ends), and the scheduled job's heartbeat
Cloudflare R2
Photos attached to reports, including photos still waiting for review, live in Cloudflare R2, Cloudflare's file storage. The phone uploads a photo straight to R2, over an encrypted connection, using an upload link the Worker signs that works for 5 minutes. The maintainer reviews it through a view link that works for 15 minutes. Everyone else sees a photo only through the Worker, which serves it only once it is approved and not under a preservation hold. A photo's stored name is made from its report and a random value, never from the uploader's user ID.
Expires automatically: every photo is deleted 90 days after upload, approved or not. A photo turned down, by review or by the automated check described under Third-party services, is deleted at once, and deleting an account deletes its photos at once. Only a photo under a preservation hold is kept longer.
Personal information collected
This website no longer collects personal information at all. It used to be possible to leave an email address in the feedback form; that form was removed, and the contact page that replaced it simply publishes an address to write to. Mail sent there is ordinary email β it is not stored in this system.
The mobile app's optional account is the second of the two record types without an expiry listed above, and the only one of the two that touches personal information: a resident who signs in hands the app an email and a name from Apple or Google, plus a Firebase user ID. The Firebase user ID keys a server-side record of notification categories, the things a resident follows, and snow-route street β it has no automatic expiry, but a resident can delete it at any time from Settings β Account β Delete account.
Where the email and name actually live is worth stating precisely, because βour serverβ means two different things here. They are held by Firebase Authentication, a Google service, inside this project's own Firebase project. They are not written to this project's own Worker or to any of its storage, KV, D1, or R2: the account record contains only what is named above, and the code that authenticates a request reads exactly one claim from the signed token β the user ID β and discards the rest. The token itself does carry an email and a name, so those values do pass through the Worker in memory on every sync; nothing keeps them. See the Privacy Policy for the full account section.
Authentication
This website has no user-facing sign-in, no accounts, no passwords, and no login forms β everything works anonymously, and preferences live on your device.
The mobile app offers an optional account, but there is still no password of our own to compromise: sign-in is delegated entirely to Sign in with Apple or Sign in with Google, via Firebase Authentication, and this project never sees or stores a credential. Server-side requests are authorized with a short-lived Firebase ID token, verified on every call; there is no session of our own to hijack. That removes entire categories of authentication risk (credential stuffing, phishing, password reuse, password databases to breach) by construction.
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.
- Notification API key: the shared secret the scraper jobs present in an
X-Api-Keyheader to send a notification or read usage stats. The check fails closed β if the secret is unset, every request is rejected rather than matching an empty value β and the comparison is constant-time so a wrong key cannot be narrowed down by timing it - Firebase service account: signs the short-lived OAuth token used to hand notifications to Firebase Cloud Messaging. Absent, every send is skipped with a warning rather than failing open
- Turnstile secret key: validates Cloudflare Turnstile challenges on the API's feedback route β the route still enforces them, but the website form that used to call it is gone
- GitHub App private key: mints short-lived installation tokens for creating GitHub issues from submissions to that same feedback route β tokens expire in 10 minutes
- GitHub issues token: a separate fine-grained token, scoped to issues on one repository, used by the feed-freshness monitor. Absent, the monitor logs instead of filing
- Slack webhook URL: where a copy of a feedback submission would be posted. Optional; unset means that step is skipped entirely
Bot protection
This website has no forms left to protect β the feedback form was removed along with the rest of the site UI. The API's feedback route still requires a valid Cloudflare Turnstile token, a privacy-preserving CAPTCHA alternative that verifies a request comes from a real browser without fingerprinting users or showing image puzzles, so the route cannot be driven by a bot even with no form in front of it.
Push notifications
Web push has been removed from this website. It no longer requests notification permission, no longer registers push subscriptions, and no longer stores push endpoints, VAPID keys, or notification preferences. The service worker has since been removed as well, so this website registers no service worker at all.
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
- Both limits are still enforced, and neither route has anything calling it (see Overview)
OWASP Top 10 self-assessment
A brief status against the OWASP Top 10 (2021) categories.
Admin endpoints β notification sends, the freshness monitor, and usage stats β require an API key via the X-Api-Key header; the maintainer's photo review and account-blocking endpoints require that key and a Cloudflare Access sign-in as well; and the mobile app's account, report, photo upload, and βI'd playβ endpoints require a Firebase ID token in an Authorization: Bearer header, verified on every call. State-changing requests (anything that is not GET or HEAD β POST, PUT, PATCH, DELETE) arriving from a browser origin other than this project's own or a localhost development origin are refused with 403 before reaching any handler. Requests that send no Origin header at all β the mobile app, command-line tools, the scraper scripts β pass that check by design, so it is a browser-origin guard rather than a substitute for the authentication above.
CORS itself is narrower than that guard, and only on preflight. An OPTIONS preflight is answered per-route: four public read-only routes (/health, /occupancy, /reports, /dev/reports) answer Access-Control-Allow-Origin: *, and every other route answers with this project's own origin β or a localhost origin during development β and varies on Origin. The actual responses do not follow that split. Replies from the original routes, including the account and admin ones, carry Access-Control-Allow-Origin: *; replies from the report, βI'd playβ, and resident photo routes carry no CORS header at all; and the maintainer's photo review and blocking routes echo this project's own origin. What actually protects those routes is the 403 origin guard above plus the API key, Access sign-in, or Firebase token each one requires β not the CORS headers. No route sends Access-Control-Allow-Credentials, so a browser will not attach a cookie to a cross-origin call regardless, and there is no cookie or session of ours for it to attach.
TLS everywhere. IP addresses used for rate limiting are SHA-256 hashed before storage and never persisted in raw form.
One SQL database service is in the request path: Cloudflare D1, which holds reports, confirmations, photo records, blocks, and βI'd playβ. Every query to it is a prepared statement, and every value that comes from a request is bound as a parameter, never pasted into the SQL text. Every request body is validated and typed before use β the account record a signed-in resident syncs, the notification registration a device sends, the report a resident files and the βI'd playβ they send, both refused outright if they carry a field the route does not expect, and the feedback and occupancy payloads whose routes remain live with no caller. Unknown fields on an account record are dropped rather than rejected, list lengths and identifier lengths are capped, and a stored value read back out of KV is re-validated with exactly the same function as one arriving from a phone.
Minimal attack surface by design: this project holds no passwords of its own β the mobile app's optional account delegates sign-in entirely to Apple and Google β much of the data expires automatically, and this website collects no personal information at all.
The personal information in the system is short: the email and name of a signed-in mobile app resident, which are held by Firebase Authentication and are never written to this project's own storage; the snow-route street in an account record, which is an address a resident chose to sync; the address a signed-in resident typed, in an account record, which is never shown to other residents; the parks and sports a resident follows in an account record, picked from a fixed list of public places, which can hint at where a resident lives; the Firebase user ID on each report, confirmation, photo record, block, preservation hold, and βI'd playβ, which ties that record to an account and is never shown to other residents; photos, which can show faces, house numbers, or license plates, and which a person reviews before anyone else sees them; an βI'd playβ, which records where an account plans to be and when, until the game ends; and an optional email address on a feedback submission, on a route that no longer has a caller. Nothing else in the system is about an identifiable person.
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 β currently rollup, hono, @hono/node-server, lodash, and cookie β are addressed via package.json overrides that force patched versions, rather than by re-resolving the whole tree, which would drag framework packages off the versions they are pinned to. Lockfile integrity is checked on every push and in CI.
This project has no password of its own anywhere: the website has no sign-in at all, and the mobile app's optional account delegates authentication entirely to Sign in with Apple / Sign in with Google via Firebase Authentication. No passwords, no credential stuffing, no phishing target, no password reset flows, no security questions to manage or leak.
JSON.parse is used on data from trusted sources (KV store, authenticated API responses) and on two kinds of request input, each checked field by field before anything uses it: the header of a sign-in token, read only to find the key that verifies its signature, and the body of an βI'd playβ request, which is refused if it carries any field but the one expected. Svelte auto-escapes all rendered content β no innerHTML or {@html} in user-facing components.
Worker errors are logged via Cloudflare Workers analytics (console.error/console.warn). No sensitive data (tokens, keys) appears in logs, and the account endpoints deliberately accept a Firebase ID token only in an Authorization header β never as a query parameter, which is how a credential ends up in a log line. The website has no analytics: the cookie-free Umami page-view script was removed along with the site UI it measured.
The worker makes outbound requests only to fixed URLs, every one of them either a literal in the source or built from a configuration value that is committed to the repository: the GitHub API, Cloudflare's Turnstile verification endpoint, Cloudflare Access's public signing keys for this project's team, Google's public signing keys and OAuth token endpoint, the Firebase Cloud Messaging send endpoint, Google Cloud Vision's SafeSearch endpoint (with an API key held as a secret), this project's own fairlawn.paragbaxi.com data files, and the Slack webhook held as a secret. No part of any of those URLs comes from a request body, a query string, or a header. No user-controlled URL is fetched server-side.
Incident response
In the event of a security incident:
- Affected data β in KV (feedback, occupancy reports, mobile app notification registrations, mobile app account records), in D1 (reports, confirmations, photo records, blocks, βI'd playβ), or in R2 (photos) β can be purged immediately via the Cloudflare dashboard or Wrangler CLI. The one exception is a photo under a preservation hold, which the law requires to be kept
- API 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 very little personal data, the scope of any reportable incident is inherently limited: no payment information, nothing at all from this website now that the site interface is gone, and, for the mobile app's optional account, an email and name that are held by Firebase Authentication rather than written to this project's own storage β so a breach of this project's own storage would not expose them. What it could expose is what that storage actually holds: notification registrations and account records, the latter including a resident's chosen snow-route street; reports, confirmations, blocks, and open βI'd playβ taps, each tied to a Firebase user ID; and photos, including any still waiting for review or kept under a preservation hold. That said, the 72-hour reporting obligation is taken seriously.
Contact
To report a security concern, email the address on the contact page. Put "SECURITY" in the subject line β those are triaged immediately.
Third-party services
- Cloudflare β Pages (static hosting), Workers (API), KV (key-value storage), D1 (SQL database), R2 (file storage for report photos), Turnstile (bot protection), Access (sign-in for the maintainer's photo review page). Cloudflare provides DDoS protection, TLS termination, and edge caching.
- Google (Firebase) β Firebase Cloud Messaging delivers every mobile app notification and issues the device tokens; Firebase Authentication holds the email, name, and user ID of a signed-in resident. Google's public signing keys and OAuth token endpoint are called to verify those sign-ins and to authorize sends. This is the one third party that holds personal information for this project.
- Google Cloud Vision β SafeSearch looks once at each photo attached to a report, before a person reviews it, and answers with how likely the photo is to be sexual or violent. A photo it finds plainly sexual or violent is turned down on the spot and deleted. The check can only turn a photo down, never approve one, and if it cannot be reached the photo simply waits for a person. Only the photo is sent β no account, user ID, place, or report goes with it β and Google states that it does not store images sent to this service.
- GitHub β source code hosting, CI/CD and the scraper jobs via GitHub Actions, and issue creation via a GitHub App with minimal scoped permissions (issues only) β used by the feed-freshness monitor, and by the API's feedback route if it ever received a submission.
- Slack β a webhook that would post a copy of a feedback submission, email address included, to the maintainer's workspace. Configured but unused, since the feedback route has no caller. It was missing from this list until August 2026.
- Anthropic and Google (Gemini) β language models called from the scraper's own quality checks in CI, to catch a scraper that has started misreading a redesigned source page. They receive publicly scraped page content only. No resident data of any kind is sent to them, and they are not in the path of any request a resident makes.
- Umami β previously used for privacy-focused, cookie-free page-view analytics. The script was removed along with the site UI in August 2026; this website now runs no analytics at all, and no vendor identifier for it remains in any page this site serves.
No data is shared with advertisers or data brokers. The services above are infrastructure providers, not data consumers. This list is meant to be exhaustive; if a service is not on it, this project does not send it anything, except where the law requires it: a photo reported under the federal child-protection law described under Cloudflare D1 goes to the National Center for Missing & Exploited Children.
Last updated: October 9, 2026
See also: Privacy Policy Β· Terms of Service