goodkeeperUnreleased
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Changed
- `maintenance_record.category` (text) is now `work_category_id` (a foreign key). Existing categories were migrated: casing variants collapsed onto the shipped vocabulary, and anything unmatched became a household-owned category. No record lost its category.
- Documented when new agent-facing capability belongs in the MCP tool catalog vs. a script. New
docs/reference/cli-vs-mcp.md, adapting PostHog's own framing of the CLI-vs-MCP tradeoff: MCP is the right call for GoodKeeper's actual end-user-facing agent surface (src/lib/mcp/tools/, no shell access, needs typed calls), but that doesn't mean every new capability belongs there โscripts/sweep.ts,scripts/seed.ts, andscripts/render-brand-icons.mjsalready draw the CI/cron/dev-tooling line correctly as scripts, not MCP tools, and new work should keep drawing it the same way. Pointers added fromCLAUDE.mdandREADME.md. - Observability direction documented: PostHog (org shared with GoodFunds) + Vercel Observability, not self-hosting, and Sentry is being phased out. After researching self-hosting requirements (Rybbit, SigNoz, Sentry, Langfuse, PostHog) against VPS pricing and each tool's own SaaS pricing, the call is to buy, not build โ GoodKeeper runs entirely on managed Vercel + Supabase with no self-run infra. New
## Observabilitysection inCLAUDE.mdrecords the decision so a future agent doesn't reach for self-hosting or treat the currently-wired Sentry SDK as the long-term direction. No code changed โ Sentry stays wired for now; this documents the target state.
Added
- Maintenance records can now be edited and deleted. Deleting a record also deletes its notes โ nothing at the database level cleaned those up, because
note.entity_iddeliberately carries no foreign key. - A controlled work-category vocabulary replaces the free-text category on maintenance records: eleven seeded defaults plus household-added ones, scoped by asset type. The cost-by-category chart no longer splits "Oil change" from "oil change" into separate slices.
- Household equipment as a third asset type โ mowers, trimmers, generators, HVAC, appliances โ with make/model/serial/purchase/warranty details, date-based maintenance schedules, and
add_equipment/update_equipment/list_equipmentMCP tools. Equipment appears in the garage alongside vehicles. - Service templates now carry an asset type, so the "apply a template" picker groups them by it โ equipment work and vehicle work appear separately instead of interleaved. Applying a typed template to a mismatched asset, or setting a mileage interval on an asset with no odometer, is now rejected rather than silently producing a schedule that can never fire.
- PostHog wired alongside Sentry โ analytics + error tracking. Client (
instrumentation-client.ts), Node server (instrumentation.ts'sonRequestErrorplus a newposthog-server.tssingleton), and the client-sideglobal-error.tsxboundary now report to both Sentry and PostHog โ Sentry is untouched, this only adds a second reporter. PostHog is Node-runtime only: their ownonRequestErrorpattern is explicitly guarded toNEXT_RUNTIME === "nodejs", with no confirmed edge-runtime story, sosentry.edge.config.tsremains the only edge coverage.NEXT_PUBLIC_POSTHOG_KEY/NEXT_PUBLIC_POSTHOG_HOSTadded viavercel envto Production and Development; Preview wasn't set due to a CLI bug in the currently-installed version (54.6.1) โ follow-up, not blocking. - Public changelog + version badge: a
/changelogpage (public, no auth) that renders thisCHANGELOG.mdfrom a small Keep-a-Changelog parser (src/lib/changelog.ts), and a.figure-mono version badge on the landing page linking to it. The displayed version is the latest released CHANGELOG header, falling back topackage.jsonversionuntil a release is cut โ the GoodFunds sibling's mechanism, adapted to GoodKeeper's design tokens. - Notifications system: per-tenant escalation and cadence selection, email and HMAC-webhook adapters, config-guard SSRF defense, fenced system-context cron sweep with Vercel scheduled-function support, and nightly digest dispatch with at-least-once semantics.
- MCP surface (
/api/mcp): 24 authenticated tools for agent access, agent-key bearer auth scoped to households, registry invariants (tool count, names, schemas), and live-smoke coverage (malformed/absent key rejection, multi-tenant failure mode sanitization). - Confirm-first ingestion pipeline: proposal schemas bound to domain write schemas, fuzzy vehicle resolution, and an open/extract/get/reject/confirm pipeline. Confirm is transactional (composing Plan 2 tx-cores), rollback-proof, and driven by
captureandconfirm_captureMCP tools. Server extractor uses AI SDK via Vercel AI Gateway (images and PDFs). - Foundation: Next.js (App Router) + TypeScript scaffold, Supabase (Auth, Storage, local Postgres via the Supabase CLI), and Drizzle ORM wired up end to end, with portable env loading and a
vitest@alias. - Multi-tenant domain layer, entirely in its own
goodkeeperPostgres schema (neverpublic), RLS-scoped throughout:- Tenancy tables and RLS isolation, backed by a
goodkeeper_private.current_tenant_ids()helper and a sharedtenantRlsPolicies()policy builder (src/db/schema/_rls.ts). - Asset / vehicle / odometer cluster, and the maintenance record/planned/schedule/template cluster, each with RLS.
- Provider and attachment tables, with provider FKs onto maintenance records and planned services.
- Notification and ingest-job tables, RLS-scoped.
- A storage bucket (
goodkeeper-attachments, private) with RLS policies, plus a seed script for global service templates. - Agent identities: agent service users are provisioned as scoped Supabase users (
app_metadata.kind = 'agent',agents.goodkeeper.localemails), with their writes fenced separately from human-user writes. - Admin (RLS-bypassing, fenced to tenant creation/invites/seeds/tests) and RLS (request-scoped,
createDrizzle(token).rls) Drizzle transaction clients.
- Tenancy tables and RLS isolation, backed by a
- Repo guardrails in
CLAUDE.md: nodrizzle-kit push, local-only tooling (never point dev config at hosted Supabase), and the drizzle migration/custom-SQL split. - Good-family repo standards: this
CONTRIBUTING.md, this changelog, a.github/workflows/ci.yml(local Supabase stack, migrate, seed, test, typecheck, schema-drift check on PRs andmain), and additiveCLAUDE.mdsections (git workflow, commands, issue-tracking convention).
Changed
- App schema moved from
publicto its owngoodkeeperschema, with its own Drizzle migration history (drizzle.__drizzle_migrations_goodkeeper), so it can coexist inside a Supabase project shared with other Good-family apps without touching their objects. - Repo renamed to GoodKeeper end to end (docs,
supabase/config.tomlproject_id, scratch-directory ignores).
Fixed
- The due engine evaluated only vehicles: schedules and planned services on any non-vehicle asset produced no due items, digest lines, or notifications. Existing `property` assets carrying schedules will begin producing due items and notifications after this change.
- Forbid non-admin creation or privilege-flip of global service templates.
- Fenced
agent_identitywrites and hardenedresolveAgentTokenagainst a security gap in agent-token resolution. - Kept the migration journal's timestamps monotonic after a migration reorder, so
drizzle-kit's ordering assumptions hold. - Let
drizzle-kitown the provider-FK migration lineage rather than hand-editing around it.
Known deferred
- Composite-FK hardening (cross-tenant FK plants): RLS scopes rows by
tenant_id, but Postgres FK checks don't consult RLS, so a caller can insert/update a row whose FK points at another tenant's row as long as its own row satisfies its own tenant-scoped check. Full list of affected columns and the two ways it can be exploited (delete-cascade griefing, unique-slot pre-emption) tracked inCLAUDE.md.