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 v7 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 v7 framework mode (HTTP +
/api/*resource routes),worker.tsfor/ws, cron, and security headers - Frontend: React 19, Tailwind CSS 4, shadcn/ui (route modules under
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: Bun, Vite, Wrangler, ESLint, Vitest
One Cloudflare Worker (worker.ts) serves everything. React Router v7 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 → worker.ts
→ /ws → Household Durable Object (WebSocket hub)
→ else → React Router (createRequestHandler)
→ clerkMiddleware + app context middleware
→ /api/* resource routes → server/api/* handlers
→ page loaders/actions (context.app + context.cloudflare)
app/routes/*.tsx— pages andapi.*resource routesserver/api/*— shared handlers (Zod validation, D1, rate limits)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()inserver/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 (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 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)
- Bun
1.3.10+ - Node.js on
PATHfor local helper scripts - Wrangler
4+ - Clerk development keys
- Optional: 1Password CLI if you use the built-in secret injection flow
bun install
bun run dev:setup
export CLERK_SECRET_KEY=sk_test_...
export CLERK_PUBLISHABLE_KEY=pk_test_...
bun run devOpen the local Vite/Workers dev URL printed by bun run dev.
bun run devdoes not expect you to maintain.dev.varsmanually.scripts/run-vite-with-dev-vars.shgenerates a temporary.dev.varsfile from the keys listed in.dev.vars.example, pulling values from the current shell environment.scripts/run-with-1password-environment.shwill automatically wrap the dev command inop runwhenOP_ENVIRONMENT_IDis available in your shell or in.op/refs.env.- If you do not use 1Password, exporting the required environment variables before
bun run devis enough.
| File / Source | Purpose |
|---|---|
.dev.vars.example |
Key manifest for local secrets consumed by the dev helper script |
.dev.vars |
Temporary file generated at runtime for local Workers bindings |
.op/refs.env or OP_ENVIRONMENT_ID |
Optional 1Password environment reference for local secret injection |
wrangler.jsonc |
Worker name, bindings, routes, cron trigger, observability, and other non-secret config |
wrangler secret put ... |
Production secret management, including CLERK_SECRET_KEY |
Current Worker bindings in wrangler.jsonc:
- 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
| Command | Description |
|---|---|
bun run dev |
Start the local Vite + Workers development server |
bun run dev:setup |
Apply local D1 migrations and seed the local database |
bun run dev:reset |
Remove local Wrangler state and re-run local setup |
bun run build |
Build the React Router app for production |
bun run deploy |
Apply remote D1 migrations, then deploy the Worker |
bun run db:generate |
Generate Drizzle migrations from schema changes |
bun run db:migrate:local |
Apply migrations to the local D1 database |
bun run db:migrate:remote |
Apply migrations to the remote D1 database |
bun run db:seed:local |
Seed the local D1 database from packages/db/seed.sql |
bun run db:studio |
Open Drizzle Studio from packages/db |
bun run typegen |
Generate React Router route types |
bun run typecheck |
Run route typegen and TypeScript checks |
bun run lint |
Run ESLint |
bun run test |
Run Vitest once |
bun run test:watch |
Run Vitest in watch mode |
app/ React Router UI, route modules (pages + `api.*` resource routes), client utilities
server/ Shared API handlers, middleware, libs, and Durable Objects (called from route modules)
packages/db/ Shared D1 schema, migrations, seed data, and DB helpers
public/ PWA icons and other static assets
scripts/ Local development and migration helper scripts
worker.ts Cloudflare Worker entrypoint with fetch + scheduled handlers
wrangler.jsonc Cloudflare configuration and bindings
CHANGELOG.md Release history
Notable route groups:
/dashboard/groceries/budget,/budget/budgets,/budget/recurring/financial— accounts and holdings (checking, savings, cash, investments, property, credit cards; legacy/accounts→/financial,/assets→/financial)/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
bun run deploy uses the default Wrangler configuration in wrangler.jsonc. In this repository, the configuration includes:
- Worker name
amigo - Smart placement enabled
- Observability and tracing enabled
- Custom domain route for
mi-amigo.com workers_devdisabled
If you want to deploy this project to a different Cloudflare account or domain, update the account, route, and binding IDs in wrangler.jsonc before deploying.
GitHub Actions in .github/workflows/ci.yaml currently run:
bun run lintbun run typecheckbun run test
on pushes to main and pull requests targeting main.
This workflow does not deploy the app.
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