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
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,542 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository19 other files it gives its agents.
- .agents/skills/vercel-react-best-practices/AGENTS.mdAGENTS.md
- .agents/skills/vercel-react-view-transitions/AGENTS.mdAGENTS.md
- .github/copilot-instructions.mdCopilot instructions
- animation-vocabularySkill
- apple-designSkill
- emil-design-engSkill
- find-animation-opportunitiesSkill
- improve-animationsSkill
- react-emailSkill
- react-routerSkill
- resendSkill
- review-animationsSkill
- supabase-postgres-best-practicesSkill
- supabase-serverSkill
- supabaseSkill
- vercel-react-best-practicesSkill
- vercel-react-view-transitionsSkill
- viteSkill
- vitestSkill
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
- Distinctive: line 61
Use these aliases throughout the codebase:
- Very specific: line 92
Every service takes the client (TypedSupabaseClient) as its first argument.
- Very specific: line 102
Every write goes through a route server action.
- Very specific: line 117
Add shouldRevalidate only where the defaults miss a reload or a loader is expensive.
- Distinctive: line 118
Don't navigate to the same URL just to tidy it: that reloads every loader on the page.
- Very specific: line 121
URL state: Prefer search params for shareable UI state (e.g.
- Distinctive: line 123
Zustand: Only for client UI state that has no server source.
- Distinctive: line 142
Create a new declarative schema file for any new standalone entity introduced
- Distinctive: line 168
Always review the generated migration - db:diff may include unintended changes (e.g., storage policy drops)
- Very specific: line 169
After db:gen-types, new fields appear in supabase/database.types.ts and flow through to model types via TablesInsert<'tablename'> and …
- Distinctive: line 170
Use DEFAULT values for new columns to ensure backwards compatibility with existing data
- Distinctive: line 197
Test files located alongside components or in dedicated test directories
- Distinctive: line 215
When any of these features are modified, update the corresponding documentation to reflect the changes.
- Distinctive: line 221
Never add explanatory comments unless absolutely necessary.
- Distinctive: line 225
If a comment is necessary, use the multi-line comments (block comments) format.
- Distinctive: line 227
Never use single-line comments except for ESLint or TypeScript directives.
- Very specific: line 229
Imports: Use @ path aliases as defined in tsconfig.json
- Distinctive: line 231
Export Style: Default exports preferred for components and hooks.
- Distinctive: line 232
Barrel Files: When adding a new component, hook, model, service, store, or util, export them from the respective barrel files and then …
- Distinctive: line 233
File Naming: Use PascalCase for React component files and lowercase-with-hyphens for directories and any other files
- Distinctive: line 234
Types Inference: Prefer inferred types over explicit annotations where possible
- Distinctive: line 235
Object Shapes: Prefer TypeScript's type aliases over interfaces for defining object shapes
≈ “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.
Files
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 formattingTesting
yarn test # Run tests with Vitest
yarn test:watch # Run tests in watch mode
yarn test:coverage # Run tests with coverageDatabase (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 formattingArchitecture 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-shellHydrateFallback) - Server data: React Router
middleware/loader/actionon the server,clientMiddleware/clientLoader/clientActionin 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 explicitid; app routes are nested under theapplayout (src/App.tsx), calendar views under thecalendarlayout (src/pages/CalendarLayout.tsx)src/pages/: Route modules (page component plusloader/action/clientLoader/clientAction/ErrorBoundaryexports), 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 inbound.store.ts)src/landing/: Marketing landing page, prerendered at/supabase/: Database migrations, schema, and generated typessupabase/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). Theapplayout puts the request's client intosupabaseContext. ItsclientMiddlewareuses the browser client, and its servermiddleware(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 passcontext.get(supabaseContext). Components and hooks that must call Supabase directly usegetSupabaseBrowserClient(). - Habits, habit details, notes, settings and OAuth consent sit under the pathless
authenticatedlayout (src/pages/AuthenticatedLayout.tsx). Its servermiddlewareredirects signed-out document requests, and itsclientMiddlewaresigned-out navigations, to/login?returnTo=…(getLoginPath). The login, register and anonymous-login pages go back toreturnTo(getReturnToPath). The calendar stays public. - Auth redirects land on
/auth/callback(src/pages/AuthCallbackPage.tsx). Its serverloaderexchanges an OAuth or PKCEcode, or verifies an emailtoken_hash, then redirects tonext, whichgetSafeRedirectPathkeeps on the same origin. Email redirects go through it too, and anextthat points back at the callback is unwrapped. Email templates link to{{ .SiteURL }}/auth/callback?token_hash={{ .TokenHash }}&type=…&next={{ .RedirectTo }}(local copies insupabase/templates/).
- The session lives in cookies (
- Loading:
- The
applayout's servermiddlewareputs the verified session user intouserContextand a once-per-request profile loader intoprofileContext(await context.get(profileContext)()). Its servermiddlewarealso reads the flash cookie (flashMiddleware). ItsclientMiddlewaresets the browser client and the session user for client loaders and the occurrence actions. - The
applayout's serverloaderreturns{ appData, toasts }: a streamed promise of{ user, profile, habits, traits }, and the flash toasts an action left before redirecting, whichAppshows. ItsclientLoader(hydrate = true) awaits it and records the first day of the week for the calendar. Read the data viauseUser/useProfile/useHabits/useTraits. - The
calendarlayout's serverloadercomputes the range from the URL, profile,Accept-Languageand thetzcookie (getRequestLocale/getRequestTimeZone) and streams the raw entries. ItsclientLoaderserves later ranges from the browser range cache and seeds the cache fromserverLoader()on the first load. Read viauseCalendarData(). - 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 thetzcookie, nevergetLocalTimeZone(). AppShellSkeletonis the app layout'sHydrateFallbackand the pending UI while entering the app.
- The
- Mutations:
- Every write goes through a route server
action. Branch on anintentand type the payloads insrc/models/*-submission.model.ts. Services that build URLs take the request'sorigin(new URL(request.url).origin), neverwindow.location. - A route whose writes change browser-cached calendar data also exports a
clientActionthat awaitsserverAction()and clears the cache infinally(/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 multipartFormDatawhen files are involved.
- Every write goes through a route server
- Submitting:
- Components submit with
useActionFetcher(onSuccess)(auseFetcherthat shows the result's toasts and callsonSuccessonce the action returns{ ok: true }), or<Form>withuseActionToasts(actionData). Pending UI comes fromfetcher.state/useNavigation()/useIsSubmitting(), notuseState. - Block a new submission on a shared fetcher while one is pending.
- Components submit with
- Results:
- Actions never show toasts. They return them in the result (
ActionResultinsrc/models/action-result.model.ts), and the submitting component shows them. A toast for the page after a redirect goes throughredirectWithToasts(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).
- Actions never show toasts. They return them in the result (
- Revalidation:
- React Router revalidates active loaders after each action.
- Loaders use React Router's default revalidation, so the
applayout 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
shouldRevalidateonly where the defaults miss a reload or a loader is expensive. Thecalendarlayout 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()(orinvalidateCalendarOccurrences/invalidateCalendarNotes) in aclientAction'sfinally. - The calendar
clientLoadercallsclaimCalendarData(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()fromreact-router(href('/habits/:habitId', { habitId })), never a hand-written path. Calendar URLs go throughgetCalendarHref(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 table05_habits.sql- Habits table06_occurrences.sql- Occurrences table07_notes.sql- Notes table08_habit_metrics.sql- Habit metrics table09_occurrence_metric_values.sql- Occurrence metric values table10_accounts.sql- User accounts table11_habit_stocks.sql- Habit stocks table12_habit_stock_metric_defaults.sql- Stock metric defaults table13_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-types3. 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_pathschanges go through theadd_occurrence_photo_paths/remove_occurrence_photo_pathfunctions, oneUPDATEeach
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_URLandSUPABASE_PUBLISHABLE_KEYare inlined into the client and server bundles through Vitedefine(declared insrc/globals.d.ts). Every client that acts for a user uses the publishable key, so Row Level Security applies.SUPABASE_SECRET_KEYbypasses Row Level Security. OnlygetSupabaseAdminClientinsrc/utils/supabase-admin.server.tsreads it, fromprocess.envat runtime. Never add it todefine, and import that module by its path, not through the@utilsbarrel, 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/clientActionfunctions and middleware are tested directly withmakeRouteArgs()from@tests(builds the request plus aRouterContextProvidercarryinguserContextandtestSupabaseClientinsupabaseContext), with@servicesmocked. Service call assertions expecttestSupabaseClientas the first argument - In test mode, Vite defines placeholder
SUPABASE_URL/SUPABASE_PUBLISHABLE_KEYvalues (TEST_ENVinvite.config.ts).getSupabaseBrowserClient()returns an empty stub under test, so mock it from@utilswhen a component's Supabase call is asserted - Components that use loader data, fetchers or
<Form>render insidecreateRoutesStubfromreact-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-routerpreset (SSR), with preview deployments for PRs yarn buildemits a bundle visualizer report; manual chunks are defined invite.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
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
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.

