A React + Cloudflare Workers web application for managing neighborhood homesites and resident contact information.
Runs the actual Worker + D1 runtime locally — mirrors production.
# One-time setup: install deps and seed local D1
npm install # frontend deps
npm --prefix worker install --ignore-scripts # Worker deps
bash d1/setup.sh # spins up local D1, applies schema + seed
# Start developing — Worker (:8787) + Vite (:5173) together
npm run devURLs:
- Frontend: http://localhost:5173
- API (Worker): http://localhost:8787
The Vite dev server proxies /api → http://localhost:8787, so develop against the frontend URL.
| Layer | Stack |
|---|---|
| Frontend | React + Vite |
| API | Cloudflare Worker (Hono) |
| Database | D1 (SQLite, local via Wrangler) |
| Auth | JWT in httpOnly cookies |
These are the local seed logins. The admin password is also the production default — change it immediately on any deployment (see Deploying to Cloudflare).
- Admin:
admin@addrbook.local/ChangeThis123! - Residents:
resident1–120@addrbook.local/Resident123!
users — id, email, password_hash, role (resident/admin), resident_id (FK), must_change_password
homesites — id, street_number, street_name, city, state, zip_code, photo BLOB
residents — id, homesite_id (FK), name, address_* (optional mailing address), created_at
phones — id, resident_id (FK), number
emails — id, resident_id (FK), address
settings — key, value (neighborhood defaults, community name, theme)
login_attempts — email, failed_count, window_start (per-email login rate limiting)
All endpoints require authentication except those marked (public); (admin) marks admin-only.
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/auth/login |
Authenticate; sets an httpOnly cookie |
| POST | /api/auth/logout |
Clear the session cookie |
| GET | /api/auth/me |
Current user info |
| PUT | /api/auth/profile |
Update own email and/or password |
| GET | /api/auth/setup-state |
Whether first-time admin setup is pending (public) |
| GET | /api/site-info |
Community name + theme (public) |
| GET | /api/manifest.webmanifest |
PWA manifest, named/themed from settings (public) |
| GET | /api/settings |
Read neighborhood defaults, community name, theme (admin) |
| PUT | /api/settings |
Update settings (admin) |
| GET | /api/homesites |
Full directory — every logged-in user |
| POST | /api/homesites |
Create a homesite (+ optional residents) (admin) |
| PUT | /api/homesites/:id |
Update a homesite (admin) |
| DELETE | /api/homesites/:id |
Delete a homesite (admin) |
| GET | /api/homesites/:id/photo |
Binary JPEG photo (or 404) |
| PUT | /api/homesites/:id/photo |
Upload photo (binary, ≤200 KB JPEG) (admin) |
| DELETE | /api/homesites/:id/photo |
Remove photo (admin) |
| GET | /api/residents |
Full directory — every logged-in user |
| GET | /api/residents/:id |
Resident + phones + emails |
| POST | /api/residents |
Create a resident (admin) |
| PUT | /api/residents/:id |
Update a resident's name/homesite (admin) |
| DELETE | /api/residents/:id |
Delete a resident (admin) |
| PUT | /api/residents/:id/contacts |
Replace phones/emails (admin or the resident) |
| PATCH | /api/residents/:id/address |
Update optional mailing address (admin or the resident) |
| GET | /api/admin/users |
List login accounts (admin) |
| POST | /api/admin/users |
Create a login for a resident (admin) |
| DELETE | /api/admin/users/:id |
Delete a login account (admin) |
| POST | /api/admin/users/:id/reset-password |
Set a new password, forcing change (admin) |
| PUT | /api/admin/users/:id/role |
Promote/demote between resident and admin (admin) |
| PUT | /api/users/:id/password |
Change a password (self-service / forced first-login) |
addr-book/
├── worker/ # Cloudflare Worker — API + serves the built site
│ ├── src/index.ts # Hono app: all /api routes + SPA fallback
│ ├── wrangler.toml # Worker config (D1 + static-assets bindings)
│ └── package.json
├── d1/ # D1 schema + seeds
│ ├── schema.sql # Full schema (apply once to a fresh DB)
│ ├── seed.sql # Demo dataset (120 homesites, ~350 residents)
│ ├── seed-admin.sql # Admin-only seed (clean production start)
│ ├── generate-seed.cjs # Regenerates seed.sql
│ └── setup.sh # One-time local D1 setup script
├── src/ # React frontend
│ ├── App.tsx
│ ├── pages/ # Home, Settings
│ ├── components/ # Layout, Login, ResidentProfile, HomesiteEditor, ui primitives
│ └── lib/ # theme, site name, photo, auth helpers
├── tests/ # Playwright API smoke suite
└── .github/workflows/ # CI (typecheck + build, API tests)
Homesites support an optional photo stored as a binary JPEG BLOB in D1.
- Photos are compressed client-side (max 640px, quality 60%, ≤200 KB)
- Displayed via
<img src="/api/homesites/:id/photo">— served as binary - Admin-only upload, remove, and replace
Deploys as a single Worker that serves both the API and the built React app
(via the [assets] binding in worker/wrangler.toml) — one origin, fits the
free tier.
Prerequisites: Node ≥22, a Cloudflare account, and wrangler login.
cd worker && npx wrangler login
# 1. Create YOUR OWN production D1 database, then copy the printed database_id
# into worker/wrangler.toml (replace the existing id — it points at another
# account's database).
npx wrangler d1 create addr-book
# 2. Create the tables in the remote database
npx wrangler d1 execute addr-book --remote --file=../d1/schema.sql
# 3. Seed the admin login only (recommended — onboard your neighborhood fresh).
npx wrangler d1 execute addr-book --remote --file=../d1/seed-admin.sql
# Or load the full demo dataset instead: --file=../d1/seed.sql
# 4. Set the session-signing secret (generated locally, never shown)
openssl rand -base64 32 | npx wrangler secret put JWT_SECRET
# 5. Build the site, then deploy (the Worker serves dist/)
cd .. && npm run build
cd worker && npx wrangler deployDeploy prints your URL: https://<worker-name>.<your-subdomain>.workers.dev.
⚠️ Change the admin password immediately.ChangeThis123!is public in this repo. Open your site, sign in asadmin@addrbook.local— you'll be forced to set a new password — and do it before sharing the URL.
Notes:
ALLOWED_ORIGINS(worker/wrangler.toml[vars]) only affects cross-origin API clients; the app is same-origin, so the default needs no change.- Redeploy after changes with
npm run buildthencd worker && npx wrangler deploy. - A custom domain can be attached in the Cloudflare dashboard.
The production instance sits behind Cloudflare Access (Zero Trust): the public hostname is gated at Cloudflare's edge, so an unauthenticated request is challenged for identity (email one-time-PIN) before it ever reaches the Worker. This is a perimeter layer in addition to the app's own login — defense in depth — and keeps the directory off the open internet.
- Configured in the Cloudflare Zero Trust dashboard (Access → Applications), not in this repo, so a fresh clone has no gate until you add one.
- Does not affect
wrangler deploy,wrangler d1 execute --remote, or local dev / CI — Access only gates the public hostname.
# Kill any running wrangler dev, then:
rm -rf ~/.wrangler/state/
bash d1/setup.shThere is no self-service password reset, so a locked-out admin is recovered
directly in the production database (requires wrangler login). This sets a
temporary password, forces a change at next login, and clears the login
rate-limit counter.
# From the repo root (after `npm install`).
# 1. Hash a temporary password — replace TEMP with one ≥10 characters
# (you'll change it right after logging in):
node -e "console.log(require('bcryptjs').hashSync('TEMP', 10))"
# 2. Write the SQL, pasting the hash from step 1 in place of PASTE_HASH_HERE.
# Using a file avoids shell-quoting issues with the '$' in bcrypt hashes:
cat > /tmp/reset.sql <<'SQL'
UPDATE users SET password_hash = 'PASTE_HASH_HERE', must_change_password = 1 WHERE email = 'admin@addrbook.local';
DELETE FROM login_attempts;
SQL
# 3. Apply to production, then remove the file:
cd worker && npx wrangler d1 execute addr-book --remote --file=/tmp/reset.sql && rm /tmp/reset.sqlThen sign in with the temporary password; you'll be required to set a new one
(min 10 characters). Omit --remote to run the same reset against the local dev DB.
- Passwords hashed with bcrypt (cost 10)
- Forced password change on first login for accounts on a temporary password — the seeded admin and any admin-created user must set a new password before using the app
- Login is rate-limited per email: 5 failed attempts within 15 min returns 429 (cleared on success)
- Auth is a JWT in an httpOnly, Secure, SameSite=Lax cookie (24h expiry) — the client never stores the token
- The seeded admin password (
ChangeThis123!) is a public default — on any deployment, sign in and change it immediately (enforced by the forced first-login change) JWT_SECRETis a Wrangler secret (wrangler secret put JWT_SECRET); for local dev it's loaded fromworker/.dev.vars(copyworker/.dev.vars.example, gitignored)- CORS is restricted to an allowlist via
ALLOWED_ORIGINS(worker/wrangler.toml[vars])
MIT