agentleFS
domhhvdomhhv/habitrack/AGENTS.md

habitrack

AGENTS.md8 starsChanged 21 days agoMITView on GitHub

Guidance for AI coding agents working in this repository. Habitrack is a modern habit tracking web application built as a progressive web app (PWA) with calendar-based visualization for habit logging. The stack includes React 19 + TypeScript, React Router v8 in framework mode (route loaders and actions own all server data), Vite, Zustand for client-only UI state, HeroUI components, Tailwind CSS v4, and Supabase as the backend. Use these aliases throughout the codebase:

  • Reads credentials

The file

Show its rules and text

We split this AGENTS.md into the rules it gives an agent

22 rules. About 0 are common, 16 are distinctive, and 6 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈16

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 6

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use these aliases throughout the codebase:

    line 61
  2. Very specific:

    Every service takes the client (TypedSupabaseClient) as its first argument.

    line 92
  3. Very specific:

    Every write goes through a route server action.

    line 102
  4. Very specific:

    Add shouldRevalidate only where the defaults miss a reload or a loader is expensive.

    line 117
  5. Distinctive:

    Don't navigate to the same URL just to tidy it: that reloads every loader on the page.

    line 118
  6. Very specific:

    URL state: Prefer search params for shareable UI state (e.g.

    line 121
  7. Distinctive:

    Zustand: Only for client UI state that has no server source.

    line 123
  8. Distinctive:

    Create a new declarative schema file for any new standalone entity introduced

    line 142
  9. Distinctive:

    Always review the generated migration - db:diff may include unintended changes (e.g., storage policy drops)

    line 168
  10. Very specific:

    After db:gen-types, new fields appear in supabase/database.types.ts and flow through to model types via TablesInsert<'tablename'> and …

    line 169
  11. Distinctive:

    Use DEFAULT values for new columns to ensure backwards compatibility with existing data

    line 170
  12. Distinctive:

    Test files located alongside components or in dedicated test directories

    line 197
  13. Distinctive:

    When any of these features are modified, update the corresponding documentation to reflect the changes.

    line 215
  14. Distinctive:

    Never add explanatory comments unless absolutely necessary.

    line 221
  15. Distinctive:

    If a comment is necessary, use the multi-line comments (block comments) format.

    line 225
  16. Distinctive:

    Never use single-line comments except for ESLint or TypeScript directives.

    line 227
  17. Very specific:

    Imports: Use @ path aliases as defined in tsconfig.json

    line 229
  18. Distinctive:

    Export Style: Default exports preferred for components and hooks.

    line 231
  19. Distinctive:

    Barrel Files: When adding a new component, hook, model, service, store, or util, export them from the respective barrel files and then …

    line 232
  20. Distinctive:

    File Naming: Use PascalCase for React component files and lowercase-with-hyphens for directories and any other files

    line 233
  21. Distinctive:

    Types Inference: Prefer inferred types over explicit annotations where possible

    line 234
  22. Distinctive:

    Object Shapes: Prefer TypeScript's type aliases over interfaces for defining object shapes

    line 235

≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
AGENTS.md

AGENTS.md

Guidance for AI coding agents working in this repository.

Project Overview

Habitrack is a modern habit tracking web application built as a progressive web app (PWA) with calendar-based visualization for habit logging. The stack includes React 19 + TypeScript, React Router v8 in framework mode (route loaders and actions own all server data), Vite, Zustand for client-only UI state, HeroUI components, Tailwind CSS v4, and Supabase as the backend.

Essential Commands

Development

yarn dev               # Start development server
yarn build            # Production build (prebuild runs typecheck, eslint:check and prettier:check)
yarn typecheck        # TypeScript type checking
yarn eslint:check     # ESLint linting
yarn eslint:fix       # Fix ESLint issues
yarn prettier:check   # Check Prettier formatting
yarn prettier:write   # Apply Prettier formatting

Testing

yarn test             # Run tests with Vitest
yarn test:watch       # Run tests in watch mode
yarn test:coverage    # Run tests with coverage

Database (Supabase)

yarn db:start          # Start local Supabase (requires Docker)
yarn db:stop           # Stop local Supabase
yarn db:restart        # Stop and start local Supabase
yarn db:status         # Check current status of local database
yarn db:reset          # Reset DB with migrations and seeds
yarn db:diff           # Generate migration from schema changes, additionally supplying migration name after -f flag
yarn db:diff:storage   # Diff the storage schema (bucket policies)
yarn db:migration:new  # Create an empty migration file
yarn db:migration:up   # Apply current migrations
yarn db:gen-types      # Generate types into supabase/database.types.ts (patched and linted afterwards)
yarn lint:sql          # SQLFluff SQL linting
yarn fix:sql           # Fix SQL formatting

Architecture Overview

Technology Stack

  • Frontend: React 19, TypeScript 6, Vite 8, React Router v8 (framework mode, SSR enabled, data loaded in server loaders, app UI rendered in the browser behind an app-shell HydrateFallback)
  • Server data: React Router middleware/loader/action on the server, clientMiddleware/clientLoader/clientAction in the browser (browser caches and the occurrence writes), fetchers and revalidation
  • Client UI state: Zustand v5 with Immer middleware (drawers, confirmation dialog, calendar filters, mobile sidebar, theme)
  • UI: HeroUI v3 (React Aria-based), Tailwind CSS v4
  • Backend: Supabase (PostgreSQL, Auth, Storage, Real-time)
  • Testing: Vitest, React Testing Library

Path Aliases

Use these aliases throughout the codebase:

  • @components → src/components
  • @pages → src/pages
  • @stores → src/stores
  • @services → src/services
  • @hooks → src/hooks
  • @models → src/models
  • @utils → src/utils
  • @const → src/constants
  • @db-types → supabase/database.types
  • @tests → tests

Key Directories

  • src/routes.ts: Route config. Every route has an explicit id; app routes are nested under the app layout (src/App.tsx), calendar views under the calendar layout (src/pages/CalendarLayout.tsx)
  • src/pages/: Route modules (page component plus loader/action/clientLoader/clientAction/ErrorBoundary exports), typed via ./+types/<File>
  • src/routes/resources/: Component-less resource routes used only by fetchers (/resources/occurrences, /resources/notes)
  • src/components/: Domain-organized components (calendar, habit, trait, account, common)
  • src/services/: Supabase API service layer, plus the calendar range caches (calendar-data.service.ts)
  • src/stores/: Zustand client UI state only (ui.store.ts, theme.store.ts, combined in bound.store.ts)
  • src/landing/: Marketing landing page, prerendered at /
  • supabase/: Database migrations, schema, and generated types
  • supabase/functions/mcp/: Remote MCP server edge function (see docs/MCP_SERVER.md)

Data and State Pattern

Server data never goes into Zustand. See docs/STATE_MIGRATION.md for the full history and rationale.

  • Supabase client:
    • The session lives in cookies (@supabase/ssr). The app layout puts the request's client into supabaseContext. Its clientMiddleware uses the browser client, and its server middleware (supabaseMiddleware) creates a server client per request and sends refreshed auth cookies back with the response.
    • Every service takes the client (TypedSupabaseClient) as its first argument. Loaders and actions pass context.get(supabaseContext). Components and hooks that must call Supabase directly use getSupabaseBrowserClient().
    • Habits, habit details, notes, settings and OAuth consent sit under the pathless authenticated layout (src/pages/AuthenticatedLayout.tsx). Its server middleware redirects signed-out document requests, and its clientMiddleware signed-out navigations, to /login?returnTo=… (getLoginPath). The login, register and anonymous-login pages go back to returnTo (getReturnToPath). The calendar stays public.
    • Auth redirects land on /auth/callback (src/pages/AuthCallbackPage.tsx). Its server loader exchanges an OAuth or PKCE code, or verifies an email token_hash, then redirects to next, which getSafeRedirectPath keeps on the same origin. Email redirects go through it too, and a next that points back at the callback is unwrapped. Email templates link to {{ .SiteURL }}/auth/callback?token_hash={{ .TokenHash }}&type=…&next={{ .RedirectTo }} (local copies in supabase/templates/).
  • Loading:
    • The app layout's server middleware puts the verified session user into userContext and a once-per-request profile loader into profileContext (await context.get(profileContext)()). Its server middleware also reads the flash cookie (flashMiddleware). Its clientMiddleware sets the browser client and the session user for client loaders and the occurrence actions.
    • The app layout's server loader returns { appData, toasts }: a streamed promise of { user, profile, habits, traits }, and the flash toasts an action left before redirecting, which App shows. Its clientLoader (hydrate = true) awaits it and records the first day of the week for the calendar. Read the data via useUser/useProfile/useHabits/useTraits.
    • The calendar layout's server loader computes the range from the URL, profile, Accept-Language and the tz cookie (getRequestLocale/getRequestTimeZone) and streams the raw entries. Its clientLoader serves later ranges from the browser range cache and seeds the cache from serverLoader() on the first load. Read via useCalendarData().
    • Pages load their own data in server loaders (habit stats, a habit, the notes list, connected identities, OAuth consent). Server code that needs the visitor's time zone reads the tz cookie, never getLocalTimeZone().
    • AppShellSkeleton is the app layout's HydrateFallback and the pending UI while entering the app.
  • Mutations:
    • Every write goes through a route server action. Branch on an intent and type the payloads in src/models/*-submission.model.ts. Services that build URLs take the request's origin (new URL(request.url).origin), never window.location.
    • A route whose writes change browser-cached calendar data also exports a clientAction that awaits serverAction() and clears the cache in finally (/habits/:habitId, /resources/notes, /resources/occurrences).
    • Files larger than a server function's request body limit (about 4.5 MB) never go to an action. Occurrence photos are compressed and uploaded from the browser by useOccurrenceFetcher, and the action receives their storage paths. Google sign-in and identity linking call the browser client directly, because they leave the page.
    • UI that belongs to one page submits to that page's route. Global UI (drawers) submits to a /resources/* route.
    • Send JSON (encType: 'application/json'), or multipart FormData when files are involved.
  • Submitting:
    • Components submit with useActionFetcher(onSuccess) (a useFetcher that shows the result's toasts and calls onSuccess once the action returns { ok: true }), or <Form> with useActionToasts(actionData). Pending UI comes from fetcher.state/useNavigation()/useIsSubmitting(), not useState.
    • Block a new submission on a shared fetcher while one is pending.
  • Results:
    • Actions never show toasts. They return them in the result (ActionResult in src/models/action-result.model.ts), and the submitting component shows them. A toast for the page after a redirect goes through redirectWithToasts(url, toasts), a flash cookie the app layout's loader reads and clears.
    • Wrap action work in runAction(promise, actionType). It returns { ok: true, toasts } or { ok: false, error, toasts } with the standard messages, without a 4xx status, so partial writes still revalidate.
    • Return a 400 only for failures that wrote nothing, e.g. failAction(title, error).
  • Revalidation:
    • React Router revalidates active loaders after each action.
    • Loaders use React Router's default revalidation, so the app layout reloads after actions, revalidate(), same-URL navigations and search-param changes, but not on plain path changes. A navigation that interrupts a revalidation still reloads it.
    • Add shouldRevalidate only where the defaults miss a reload or a loader is expensive. The calendar layout has one, because it reads its child routes' params.
    • Don't navigate to the same URL just to tidy it: that reloads every loader on the page.
    • Writes that change calendar data clear the browser cache with clearCalendarData() (or invalidateCalendarOccurrences/invalidateCalendarNotes) in a clientAction's finally.
    • The calendar clientLoader calls claimCalendarData(userId), which clears the cache when the signed-in user changed. Server sign-ins send no auth event to the browser.
  • URL state: Prefer search params for shareable UI state (e.g. /notes?pages=N).
  • Route URLs: Build every link, navigation, fetcher action and redirect with href() from react-router (href('/habits/:habitId', { habitId })), never a hand-written path. Calendar URLs go through getCalendarHref(view, date?) from @utils.
  • Zustand: Only for client UI state that has no server source.

Database Schema Changes - Quick Reference

1. Modify the Declarative Schema

Edit the appropriate schema file in supabase/schemas/:

  • 04_traits.sql - Traits table
  • 05_habits.sql - Habits table
  • 06_occurrences.sql - Occurrences table
  • 07_notes.sql - Notes table
  • 08_habit_metrics.sql - Habit metrics table
  • 09_occurrence_metric_values.sql - Occurrence metric values table
  • 10_accounts.sql - User accounts table
  • 11_habit_stocks.sql - Habit stocks table
  • 12_habit_stock_metric_defaults.sql - Stock metric defaults table
  • 13_occurrence_stock_usages.sql - Occurrence stock usages table

Create a new declarative schema file for any new standalone entity introduced

2. Run Migration Workflow

# Stop local db (required before diffing)
yarn db:stop

# Generate migration file from schema diff
yarn db:diff -f <migration_name>

# Review the generated migration in supabase/migrations/
# Ensure it only contains intended changes (remove any unrelated drops/alters)

# Start db
yarn db:start

# Apply the migrations
yarn db:migration:up

# Regenerate TypeScript types
yarn db:gen-types

3. Key Notes

  • Always review the generated migration - db:diff may include unintended changes (e.g., storage policy drops)
  • After db:gen-types, new fields appear in supabase/database.types.ts and flow through to model types via TablesInsert<'tablename'> and TablesUpdate<'tablename'>
  • Use DEFAULT values for new columns to ensure backwards compatibility with existing data

Storage Configuration

Supabase storage buckets:

  • habit_icons: Public bucket (100KB limit per file)
  • occurrence_photos: Private bucket (5MB limit per file), uploaded from the browser into <userId>/<habitId>/. photo_paths changes go through the add_occurrence_photo_paths/remove_occurrence_photo_path functions, one UPDATE each

Environment Requirements

  • Node.js 24.22.1
  • Docker (for local Supabase)
  • Yarn is used as a package manager and is automatically available via Corepack (bundled with Node.js)

Supabase Keys

  • SUPABASE_URL and SUPABASE_PUBLISHABLE_KEY are inlined into the client and server bundles through Vite define (declared in src/globals.d.ts). Every client that acts for a user uses the publishable key, so Row Level Security applies.
  • SUPABASE_SECRET_KEY bypasses Row Level Security. Only getSupabaseAdminClient in src/utils/supabase-admin.server.ts reads it, from process.env at runtime. Never add it to define, and import that module by its path, not through the @utils barrel, so it stays out of the client bundle.

Testing Approach

  • Vitest with jsdom environment for unit tests
  • React Testing Library for component testing
  • Route loader/action/clientLoader/clientAction functions and middleware are tested directly with makeRouteArgs() from @tests (builds the request plus a RouterContextProvider carrying userContext and testSupabaseClient in supabaseContext), with @services mocked. Service call assertions expect testSupabaseClient as the first argument
  • In test mode, Vite defines placeholder SUPABASE_URL/SUPABASE_PUBLISHABLE_KEY values (TEST_ENV in vite.config.ts). getSupabaseBrowserClient() returns an empty stub under test, so mock it from @utils when a component's Supabase call is asserted
  • Components that use loader data, fetchers or <Form> render inside createRoutesStub from react-router
  • Test files located alongside components or in dedicated test directories
  • Coverage reports available with yarn test:coverage

Build & Deploy

  • Vercel via the @vercel/react-router preset (SSR), with preview deployments for PRs
  • yarn build emits a bundle visualizer report; manual chunks are defined in vite.config.ts

Feature Documentation

End-to-end documentation for complex features (database → models → services → route loaders/actions → UI):

  • docs/METRICS.md - Habit metric definitions and occurrence metric values
  • docs/STOCKS.md - Habit stocks, stock metric defaults, occurrence stock usages, auto-depletion
  • docs/COSTS.md - Occurrence cost calculation, period summaries, multi-currency handling
  • docs/STATE_MIGRATION.md - Zustand → React Router data APIs migration record and the roadmap for full-stack React Router features
  • docs/MCP_SERVER.md - Remote MCP server, OAuth flow and consent page

When any of these features are modified, update the corresponding documentation to reflect the changes.

Important Conventions

Comments

Never add explanatory comments unless absolutely necessary.

Code must be clean, readable, and self-documenting.

If a comment is necessary, use the multi-line comments (block comments) format. JSDoc and TODO comments are allowed.

Never use single-line comments except for ESLint or TypeScript directives.

  • Imports: Use @ path aliases as defined in tsconfig.json
  • Components: Arrow functions preferred over function declarations
  • Export Style: Default exports preferred for components and hooks; named exports preferred for service methods
  • Barrel Files: When adding a new component, hook, model, service, store, or util, export them from the respective barrel files and then import using a corresponding import alias
  • File Naming: Use PascalCase for React component files and lowercase-with-hyphens for directories and any other files
  • Types Inference: Prefer inferred types over explicit annotations where possible
  • Object Shapes: Prefer TypeScript's type aliases over interfaces for defining object shapes

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.