The short version: Fair Lawn Apps runs on Cloudflare's edge network with HTTPS everywhere, stores minimal data with automatic expiration in almost every case, and never handles sensitive personal information. 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 two things are written to this project's own storage today, both from mobile app residents who opt in: notification tokens and account records. A third 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. Three other routes β gym occupancy check-ins, interest counts, 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 any of them, so nothing arrives at any of the three. 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
Everything this project stores server-side lives in one Cloudflare Workers KV namespace. Most of it 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
- Interest counts and their per-voter dedup records β end of day plus a small buffer; 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.
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 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, followed sections, 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 its KV store: the account record contains only the four fields 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
- Interest signals: one vote per event per IP, enforced by a dedup record keyed on a SHA-256 hash of the IP. This is the one place an IP-derived value is stored next to what someone did rather than purely as a counter, so it is called out here as well as in the Privacy Policy; it expires at end of day, and the raw IP is never persisted
- Rate-limiting keys expire automatically via KV TTLs
- All three limits are still enforced, and none of the three routes 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, and the mobile app's account 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, /interest, /interest/batch) 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: every real reply this API sends, on every route including the account and admin ones, carries Access-Control-Allow-Origin: *. Describing this API as having per-endpoint CORS would therefore be wrong, and an earlier version of this page came close to it. What actually protects those routes is the 403 origin guard above plus the API key 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.
No SQL database is in the request path. Every request body is validated and typed before use β the account record a signed-in resident syncs, the notification registration a device sends, and the feedback, occupancy, and interest 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 β most 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; 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 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 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, Google's public signing keys and OAuth token endpoint, the Firebase Cloud Messaging send endpoint, 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 KV data (feedback, occupancy reports, interest counts, mobile app notification registrations, mobile app account records) can be purged immediately via the Cloudflare dashboard or Wrangler CLI
- 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 KV namespace would not expose them. What it could expose is what that namespace actually holds: notification registrations and account records, the latter including a resident's chosen snow-route street. 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), Turnstile (bot protection). 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.
- 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.
Last updated: August 24, 2026
See also: Privacy Policy Β· Terms of Service