๐Ÿถ Dogfood โ€” internal environment. Outbound actions may be real.
goodkeeper

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.

Unreleased

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, and scripts/render-brand-icons.mjs already 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 from CLAUDE.md and README.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 ## Observability section in CLAUDE.md records 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_id deliberately 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_equipment MCP 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's onRequestError plus a new posthog-server.ts singleton), and the client-side global-error.tsx boundary now report to both Sentry and PostHog โ€” Sentry is untouched, this only adds a second reporter. PostHog is Node-runtime only: their own onRequestError pattern is explicitly guarded to NEXT_RUNTIME === "nodejs", with no confirmed edge-runtime story, so sentry.edge.config.ts remains the only edge coverage. NEXT_PUBLIC_POSTHOG_KEY/NEXT_PUBLIC_POSTHOG_HOST added via vercel env to 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 /changelog page (public, no auth) that renders this CHANGELOG.md from 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 to package.json version until 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 capture and confirm_capture MCP 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 goodkeeper Postgres schema (never public), RLS-scoped throughout:
    • Tenancy tables and RLS isolation, backed by a goodkeeper_private.current_tenant_ids() helper and a shared tenantRlsPolicies() 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.local emails), 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.
  • Repo guardrails in CLAUDE.md: no drizzle-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 and main), and additive CLAUDE.md sections (git workflow, commands, issue-tracking convention).

Changed

  • App schema moved from public to its own goodkeeper schema, 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.toml project_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_identity writes and hardened resolveAgentToken against 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-kit own 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 in CLAUDE.md.