Cloudflare-native household management app for shared budgeting, groceries, assets, debts, and calendar planning. The app runs as a single Worker-backed application with React Router v8 framework mode (SSR, loaders, actions, and /api/* resource routes), real-time household updates over WebSockets, and offline-first grocery syncing.
- Shared household dashboard and setup flow
- Budget tracking with transactions, budgets, and recurring entries
- Grocery list management with tags, optimistic updates, and offline sync
- Asset and debt tracking
- Calendar aggregation for household activity
- Household settings, member roles, and account restore flows
- Real-time updates through a household-scoped Durable Object WebSocket hub
- Runtime: Cloudflare Workers
- Server: React Router v8 framework mode (HTTP +
/api/*resource routes),apps/web/worker.tsfor/ws, cron, and security headers - Frontend: React 19, Tailwind CSS 4, shadcn/ui (route modules under
apps/web/app/routes/) - Data: Cloudflare D1 (SQLite) with Drizzle ORM
- Realtime and caching: Durable Objects, KV, Workers Cache API
- Offline: Dexie +
vite-plugin-pwa - Auth: Clerk
- Tooling: pnpm workspaces, Turborepo, Vite, Wrangler, ESLint, Vitest
One Cloudflare Worker (apps/web/worker.ts) serves everything. React Router v8 framework mode handles SSR, page loaders/actions, and /api/* JSON resource routes. There is no separate HTTP framework.
- Single Worker — RR plus Worker-only concerns (
/ws, cron, security headers) in one deployable unit - Integer cents — all money in D1 is stored as integer cents (never floats)
- Application-level tenancy — every D1 query must filter with
scopeToHousehold()from@amigo/db(no DB-level RLS) - Optimistic groceries — Dexie (IndexedDB) for instant UI; background sync via
/api/sync(max 10 mutations per request)
Client → apps/web/worker.ts
→ /ws → Household Durable Object (WebSocket hub)
→ else → React Router (createRequestHandler)
→ clerkMiddleware + app context middleware
→ /api/* resource routes → apps/web/server/api/* handlers
→ page loaders/actions (context.app + context.cloudflare)
apps/web/app/routes/*.tsx— pages andapi.*resource routesapps/web/server/api/*— shared handlers (Zod validation, D1, rate limits)apps/web/server/durable-objects/household.ts— per-household WebSocket hub (Hibernation API)packages/db/— Drizzle schema, migrations,getDb(),scopeToHousehold()
Sync-enabled tables use deletedAt for soft deletes. Schema lives under packages/db/src/schema/.
- Client opens
/ws→ routed to the household’s Durable Object - Mutations call
broadcastToHousehold()inapps/web/server/lib/realtime.ts - Connected clients receive an event and revalidate loaders
- Optional
senderIdskips the connection that initiated the mutation
@clerk/react-routerfor middleware, loaders, and client provider- Session cache in KV (24h TTL, keyed by Clerk user id)
- First login auto-creates household + user rows in D1
KV-backed rate limits (apps/web/server/middleware/rate-limit.ts):
| Preset | Limit | Use case |
|---|---|---|
| MUTATION | 30/min | Standard writes |
| BULK | 10/min | Bulk operations |
| SENSITIVE | 10/min | Settings, members |
| READ | 60/min | List reads |
Household roles (owner > admin > member): canManageHousehold and canManageMembers require owner or admin; canTransferOwnership is owner-only. Helpers live in apps/web/server/lib/permissions.ts.
- Local state in Dexie; sync queue flushed in chunks to
/api/sync - Conflicts: server-wins with field-level merge
- PWA via
vite-plugin-pwa(NetworkFirst for API, CacheFirst for static assets)
- pnpm 11.3.0 (run
corepack enableto use the version pinned inpackage.json) - Node.js on
PATHfor local helper scripts - Wrangler
4+ - Clerk development keys
- 1Password CLI and a 1Password Environment for secrets (recommended)
pnpm install
pnpm run dev:setup
cp apps/web/.op/refs.env.example apps/web/.op/refs.env
# Set OP_ENVIRONMENT_ID to the amigo (dev) Environment UUID from 1Password.
pnpm run dev:verify
pnpm run devOpen the local Vite/Workers dev URL printed by pnpm run dev.
- Copy
apps/web/.op/refs.env.exampletoapps/web/.op/refs.envand setOP_ENVIRONMENT_IDto theamigo (dev)Environment UUID from 1Password. pnpm run devusesop run --environmentto inject secrets intoprocess.env; the Cloudflare Vite plugin reads them directly (CLOUDFLARE_INCLUDE_PROCESS_ENV). Do not mount a.dev.varsfile.pnpm run dev:verifychecks that every key fromapps/web/.dev.vars.exampleis present (names only; no secret values printed).- All secrets and deploy identifiers live in 1Password Environments; the repo only tracks variable names in
*.examplemanifests. pnpm run deployalso usesop runand renders ignoredapps/web/.wrangler.deploy.jsoncfrom environment variables, so live Cloudflare IDs and domains do not need to live in git.
Cursor cloud agents should not copy individual app secrets into the Cursor dashboard. Use the same bootstrap pattern as Cloudflare Workers Builds:
- Create a read-only 1Password service account scoped to
amigo (dev)only (separate from the prod Workers Builds token). - In Cursor → Cloud Agents → your amigo environment → Secrets, add only:
| Secret | Cursor type | Value |
|---|---|---|
OP_SERVICE_ACCOUNT_TOKEN |
Runtime Secret | Read-only service account with access to amigo (dev) only |
OP_ENVIRONMENT_ID |
Environment Variable | UUID of the amigo (dev) Environment |
Do not add Clerk keys, VAPID keys, Cloudflare binding IDs, or other keys from apps/web/.dev.vars.example to Cursor. Commands like pnpm run dev and pnpm run dev:verify inject them via op run --environment through scripts/run-with-1password-environment.sh. Cloud agents resolve OP_ENVIRONMENT_ID from Cursor secrets (not from gitignored apps/web/.op/refs.env).
For the cloud environment install/update command, use pnpm install && pnpm run dev:setup (local D1 only; no app secrets required). After bootstrap secrets are set, run pnpm run dev:verify to confirm the Environment is complete (names only).
Before opening a PR from a cloud agent: pnpm run dev:verify, pnpm run typecheck, pnpm run test:unit, and relevant pnpm run test:integration when touching Workers/D1/DO code.
| File / Source | Purpose |
|---|---|
apps/web/.dev.vars.example |
Key manifest for local dev (op run + dev:verify) |
apps/web/.deploy.env.example |
Deploy binding IDs and Worker vars (rendered into apps/web/.wrangler.deploy.jsonc) |
apps/web/.wrangler.secrets.example |
Worker secrets for local dev (secrets.required) and deploy (wrangler deploy --secrets-file) |
apps/web/.op/refs.env.example |
Template for local OP_ENVIRONMENT_ID reference (copy to gitignored apps/web/.op/refs.env) |
apps/web/.op/refs.env or OP_ENVIRONMENT_ID |
1Password Environment reference for op run (dev locally / cloud agents, prod in Workers Builds) |
apps/web/wrangler.jsonc |
Public-safe Wrangler template used for local development and documentation |
apps/web/.wrangler.deploy.jsonc |
Ignored production config rendered at deploy time from environment variables |
Current Worker bindings in the public apps/web/wrangler.jsonc template:
- D1 database binding:
DB(amigo-db) - KV namespace:
CACHE - Durable Object:
HOUSEHOLD - Static asset binding:
ASSETS - Weekly cron: Sunday at
03:00 UTCfor audit log pruning - Daily cron:
04:23 UTCfor recurring transaction processing
| Command | Description |
|---|---|
pnpm run dev |
Start the local Vite + Workers development server |
pnpm run dev:verify |
Verify the amigo (dev) Environment via op run (names only) |
pnpm run dev:setup |
Apply local D1 migrations and seed the local database |
pnpm run dev:reset |
Remove local Wrangler state and re-run local setup |
pnpm run build |
Build the React Router app for production |
pnpm run deploy |
Apply remote D1 migrations, then deploy the Worker |
pnpm run db:generate |
Generate Drizzle migrations from schema changes |
pnpm run db:migrate:local |
Apply migrations to the local D1 database |
pnpm run db:migrate:remote |
Apply migrations to the remote D1 database |
pnpm run db:seed:local |
Seed the local D1 database from packages/db/seed.sql |
pnpm run db:studio |
Open Drizzle Studio from packages/db |
pnpm run typegen |
Generate React Router route types |
pnpm run typecheck |
Run route typegen and TypeScript checks |
pnpm run lint |
Run ESLint |
pnpm run test |
Run the unit and Workers integration Vitest suites |
pnpm run test:watch |
Run Vitest in watch mode |
apps/web/ React Router UI, Worker entrypoint, and Cloudflare config
apps/web/app/ Route modules, frontend components, and client utilities
apps/web/server/ Shared API handlers, middleware, libs, and Durable Objects
packages/db/ Shared D1 schema, migrations, seed data, and DB helpers
scripts/ Local development and migration helper scripts
turbo.json Turborepo task graph
CHANGELOG.md Release history
Notable route groups:
/dashboard/groceries/financial— transactions, recurring rules, budgets, accounts, and debts (legacy/budget→/financial,/accounts→/financial/accounts,/assets→/financial/accounts)/financial/debts— debts (legacy/debts→/financial/debts)/calendar— redirects to/dashboard/settings/setup/restore-account
Notable API groups:
/api/health/api/setup/api/groceries/api/tags/api/transactions/api/budgets/api/recurring/api/assets/api/debts/api/members/api/settings/api/sync/api/calendar/api/restore/api/audit
pnpm run deploy first renders apps/web/.wrangler.deploy.jsonc from the current shell environment, then uses that ignored file for remote D1 migrations and the Worker deploy. The committed apps/web/wrangler.jsonc stays as a public-safe template.
All production and development secrets are stored in 1Password Environments only. The repo tracks names in apps/web/.deploy.env.example and apps/web/.wrangler.secrets.example. Do not use wrangler secret put or the Cloudflare dashboard to author secrets — pnpm run deploy renders a temporary secrets file from op run and passes it to wrangler deploy --secrets-file.
amigo (dev) — local pnpm run dev (OP_ENVIRONMENT_ID in apps/web/.op/refs.env).
amigo (prod) — Cloudflare Workers Builds (OP_ENVIRONMENT_ID build secret).
Each Environment should define every key from both manifests (dev vs prod values differ, e.g. pk_test_ vs pk_live_).
Local deploy: copy apps/web/.op/refs.env.example to apps/web/.op/refs.env, set OP_ENVIRONMENT_ID to amigo (prod), then pnpm run deploy. The deploy renderer defaults APP_ENV to production; if you intentionally deploy against dev-scoped bindings, set APP_ENV=development before running pnpm run deploy.
Git-connected production deploys must not use the placeholder apps/web/wrangler.jsonc alone. After #53, the build must render apps/web/.wrangler.deploy.jsonc with real binding IDs.
- Use the
amigo (prod)1Password Environment (production binding IDs and deploy keys). Keepamigo (dev)for local development only. - Add every key from
apps/web/.deploy.env.exampleandapps/web/.wrangler.secrets.exampletoamigo (prod)in the 1Password app. - Add Workers Builds secrets:
OP_SERVICE_ACCOUNT_TOKEN(read-only service account) andOP_ENVIRONMENT_IDset to theamigo (prod)Environment UUID from 1Password. - Workers Builds commands (Worker Settings → Build):
- Build:
pnpm install && pnpm run build - Deploy:
pnpm run deploy
- Build:
If Workers Builds was still running plain wrangler deploy against the template config, the post-merge failure is expected: add the bootstrap secrets and set the deploy command above.
The generated deploy config contains:
- Worker name
amigo - Smart placement enabled
- Observability and tracing enabled
- Custom domain route from
CLOUDFLARE_CUSTOM_DOMAIN workers_devdisabled
If you want to deploy this project to a different Cloudflare account or domain, change the deploy-time environment variables instead of editing the committed apps/web/wrangler.jsonc.
GitHub Actions in .github/workflows/ci.yaml currently run the root compatibility scripts, which fan out through Turborepo and workspace filters:
pnpm run lintpnpm run typecheckpnpm run typegenpnpm run db:migrate:localpnpm run test
on pushes to any branch and pull requests targeting main.
This workflow does not deploy the app.
For security hardening, main uses a repository ruleset: pull requests with review, required CI and CodeQL checks, signed commits, and linear history. Merge commits are disabled at the repository level; changes land via squash or rebase merge only. See Contributing § Main branch policy for contributor-facing details.
Copyright © 2026 James Cadena.
GNU Affero General Public License v3.0 (SPDX AGPL-3.0). AGPL is a strong copyleft license: modified versions must stay under the same license when conveyed, and if you run a modified version as a network service for others, you generally must offer them the corresponding source as well (see section 13 of the license). This is not legal advice; read the full text in LICENSE.
- Changelog — release history
- Contributing — development setup, PR expectations, AGPL note
- Security — reporting vulnerabilities responsibly