diff --git a/.cursor/rules/dev-rules.mdc b/.cursor/rules/dev-rules.mdc index 39af7db2..e859967f 100644 --- a/.cursor/rules/dev-rules.mdc +++ b/.cursor/rules/dev-rules.mdc @@ -9,7 +9,7 @@ alwaysApply: true - express node api server `/backend/api` - one off scripts, like migrations `/backend/scripts` - supabase postgres. schema in `/backend/supabase` - - supabase-generated types in `/backend/supabase/schema.ts` + - supabase-generated types in `/common/src/supabase/schema.ts` - files shared between backend directories `/backend/shared` - anything in `/backend` can import from `shared`, but not vice versa - files shared between the frontend and backend in `/common` @@ -313,7 +313,7 @@ export const placeBet: APIHandler<'bet'> = async (props, auth) => { } ``` -And finally, you need to register the handler in `backend/api/src/routes.ts` +And finally, you need to register the handler in the `handlers` map in `backend/api/src/app.ts` ```ts import { placeBet } from './place-bet' diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index aa08acad..f0ccceea 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -14,7 +14,7 @@ Compass (compassmeet.com) is a transparent dating platform for forming deep, aut - **Shared backend utilities** `/backend/shared` - **Email functions** `/backend/email` - **Database schema** `/backend/supabase` - - Supabase-generated types in `/backend/supabase/schema.ts` + - Supabase-generated types in `/common/src/supabase/schema.ts` - **Files shared between frontend and backend** `/common` - Types (User, Profile, etc.) and utilities - Try not to add package dependencies to common diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..57395f5c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,116 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project + +Compass (compassmeet.com) is a transparent platform for forming deep, authentic 1-on-1 connections. It is a +Yarn-workspaces monorepo: Next.js + React web frontend, Express API backend, Capacitor Android app, and shared +TypeScript packages. Backed by Supabase (PostgreSQL), Firebase (auth + media storage), hosted on Vercel (web) and Google +Cloud (API). + +## Commands + +Run from the repo root unless noted. Package manager is **yarn** (`yarn install --frozen-lockfile`). + +```bash +yarn dev # Run web + API against the shared remote dev DB (visit http://localhost:3000) +yarn dev:isolated # Run with local Supabase + Firebase emulators (needs Docker, Supabase CLI, Java 21+, Firebase CLI) +yarn lint # ESLint across web, common, backend/{api,shared,email} +yarn lint-fix # ESLint --fix across the same +yarn typecheck # tsc --noEmit across all packages +yarn prettier # Format the repo +yarn test # Jest unit + integration across all workspaces +yarn test:coverage # Jest with coverage +yarn test:e2e # Playwright E2E (spins up services) +yarn test:e2e:dev # Playwright against an already-running dev server +``` + +Per-package and single tests (the workspace `test` script already passes `--config`, so append Jest args): + +```bash +yarn --cwd=common test # one workspace +yarn --cwd=backend/api test path/to/file.unit.test.ts # one file +yarn --cwd=web test -t "renders profile card" # by test name +``` + +Database migrations and types: + +```bash +./scripts/migrate.sh supabase/migrations/.sql # apply a migration +yarn --cwd=backend/api regen-types-dev # regenerate Supabase types (dev) into common/src/supabase/schema.ts +``` + +## Architecture + +### Workspaces and import boundaries (enforced by convention, important) + +- `/web` — Next.js/React/Tailwind frontend (`pages/`, `components/`, `hooks/`, `lib/`). +- `/backend/api` — Express REST + WebSocket server. Handlers are one file per endpoint in `src/`. +- `/backend/shared` — backend-only utilities (DB init, monitoring, push). Anything in `/backend` may import from + `shared`, **not vice versa**. +- `/backend/email` — React-email templates and send routines. +- `/common` — types (User, Profile, etc.) and pure utilities shared by frontend and backend. `/web` and `/backend` + import from `/common`, **never the reverse**. Avoid adding package dependencies here. +- `/supabase` — active Postgres migrations (`migrations/`), `config.toml`, `seed.sql` for local/isolated dev. +- `/backend/supabase` — per-table SQL definitions and the `make regen-types` targets. +- `/android` — Capacitor wrapper around the web build. + +Request flow: React component → `useAPIGetter`/`api()` → HTTP → Express handler (auth middleware → handler) → Postgres → +response → React state. + +### Adding an API endpoint (3 steps, spans packages) + +1. Define the endpoint schema (method, `authed`, `props` as a Zod object, `returns`) in `common/src/api/schema.ts`. +2. Implement the handler `export const x: APIHandler<'endpoint-name'>` in its own file under `backend/api/src/`. +3. Register it in the `handlers` map in `backend/api/src/app.ts` (around line 583). + +### Database access — two clients + +- **Backend** (`createSupabaseDirectClient()` from `shared/supabase/init`): raw SQL via pg-promise (`pg.oneOrNone`, + `pg.manyOrNone`). Web cannot do this. +- **Frontend** (`db` from `web/lib/supabase/db`): the Supabase JS client (`db.from('table').select(...)`), a PostgREST + wrapper. +- Never string-concatenate SQL. Use the helpers in `shared/supabase/utils` (`insert`, `update`, `updateData`, + `bulkUpsert`, ...) or compose with `renderSql`/`select`/`from`/`where` from `shared/supabase/sql-builder.ts`. SQL is + written lowercase by convention. + +### Frontend conventions + +- Many small composable components over large ones. Export the main component at the top of the file; name it after the + file (`profile-card.tsx` → `ProfileCard`). +- Client data fetching: `useAPIGetter('endpoint', props)` (returns `{data, refresh}`, cached in memory). Server-side: + `api('endpoint', props)` inside `getStaticProps`/`getServerSideProps`. +- Prefer `usePersistentInMemoryState` / `usePersistentLocalState` over `useState` when navigating back to a page should + feel instant. +- Live updates use WebSockets via `useApiSubscription` (topics broadcast from + `backend/shared/src/websockets/helpers.ts`). +- Prefer lodash (`keyBy`, `uniq`, `uniqBy`) over hand-rolled loops/Sets. + +### Internationalization + +`const t = useT()` (from `web/lib/locale`), then `t('key', 'English fallback')`. Translation JSON lives in +`common/messages/` (`de.json`, `fr.json`; English is the inline fallback). To add a language see `docs/development.md` +and the `LOCALES` dict in `common/src/constants.ts`. + +### Timestamps + +Use `Date` everywhere in TypeScript; `TIMESTAMPTZ` in Postgres (pg-promise converts automatically). The Zod endpoint +schema handles Date↔string serialization across the wire. When persisting to localStorage, convert string back to Date +on load. + +## Conventions to follow + +- Don't add `sleep()` delays for "eventual consistency" — rely on transactional integrity (e.g. user + profile + options + are created in one transaction in `create-user-and-profile.ts`). +- Don't split into multiple API calls when data can be batched in one transaction; fetch profile options. +- Don't use `console.log` — use `debug()` from `common/logger`. +- Scripts in `/backend/scripts` run inside `runScript(async ({pg}) => ...)` which loads secrets into `process.env`. + Anything that mutates backend state or schema should generally be run by the user, not Claude. + +## Detailed docs + +`docs/knowledge.md` (architecture + code patterns), `docs/internationalization.md` (adding languages), +`docs/profile_fields.md` (adding profile fields), `docs/TESTING.md` (test layout and practices), +`docs/DATABASE_SCHEMA.md`, `docs/PERFORMANCE_OPTIMIZATION.md`, `docs/DATABASE_CONNECTION_POOLING.md`, +`docs/TROUBLESHOOTING.md`, `docs/Next.js.md`. Per-area READMEs in `web/`, `backend/api/`, `backend/email/`. diff --git a/docs/development.md b/docs/development.md index 83ab11c7..1e8d793e 100644 --- a/docs/development.md +++ b/docs/development.md @@ -15,57 +15,5 @@ See those other useful documents as well: - [PERFORMANCE_OPTIMIZATION.md](PERFORMANCE_OPTIMIZATION.md) for performance best practices - [DATABASE_CONNECTION_POOLING.md](DATABASE_CONNECTION_POOLING.md) for database connection management - [TROUBLESHOOTING.md](TROUBLESHOOTING.md) for resolving common development issues - -### Adding a new profile field - -A profile field is any variable associated with a user profile, such as age, politics, diet, etc. You may want to add a -new profile field if it helps people find better matches. - -To do so, you can add code in a similar way as -in [this commit](https://github.com/CompassConnections/Compass/commit/940c1f5692f63bf72ddccd4ec3b00b1443801682) for the -`religion` field. If you also want people to filter by that profile field, you'll also need to add it to the search -filters, as done -in [this commit](https://github.com/CompassConnections/Compass/commit/a4bb184e95553184a4c8773d7896e4b570508fe5) (for the -`religion` field as well). - -Note that you will also need to add a column to the `profiles` table in the dev database before running the code; you -can do so via this SQL command (change the type if not `TEXT`): - -```sql -ALTER TABLE profiles -ADD COLUMN profile_field TEXT; -``` - -Store it in `add_profile_field.sql` in the [migrations](../backend/supabase/migrations) folder and -run [migrate.sh](../scripts/migrate.sh) from the root folder: - -```bash -./scripts/migrate.sh backend/supabase/migrations/add_profile_field.sql -``` - -Then sync the database types from supabase to the local files (which assist Typescript in typing): - -```bash -yarn regen-types dev -``` - -That's it! - -### Adding a new language - -Adding a new language is very easy, especially with translating tools like large language models (ChatGPT, etc.) which -you can use as first draft. - -- Add the language to the LOCALES dictionary in [constants.ts](../common/src/constants.ts) (the key is the locale code, - the value is the original language name (not in English)). -- Duplicate [fr.json](../web/messages/fr.json) and rename it to the locale code (e.g., `de.json` for German). Translate - all the strings in the new file (keep the keys identical). LLMs like ChatGPT may not be able to translate the whole - file in one go; try to copy-paste by batch of 300 lines and ask the LLM to - `translate the values of the json above to (keep the keys unchanged)`. In order to fit the bottom - navigation bar on mobile, make sure the values for those keys are less than 10 characters: "nav.home", " - nav.messages", "nav.more", "nav.notifs", "nav.people". -- Duplicate the [fr](../web/public/md/fr) folder and rename it to the locale code (e.g., `de` for German). Translate all - the markdown files in the new folder. To do so, you can copy-paste each file into an LLM and ask it to - `translate the markdown above to `. - -That's all, no code needed! +- [profile_fields.md](profile_fields.md) for adding new profile fields +- [internationalization.md](internationalization.md) for adding new languages diff --git a/docs/internationalization.md b/docs/internationalization.md new file mode 100644 index 00000000..0fcad84f --- /dev/null +++ b/docs/internationalization.md @@ -0,0 +1,20 @@ +# Adding a new language + +Adding a new language is very easy, especially with translating tools like large language models (ChatGPT, Claude, etc.) +which +you can use as first draft. + +- Add the language to the LOCALES dictionary in [constants.ts](../common/src/constants.ts) (the key is the locale code, + the value is the original language name (not in English)). +- Duplicate [fr.json](../common/messages/fr.json) and rename it to the locale code (e.g., `de.json` for German). + Translate + all the strings in the new file (keep the keys identical). LLMs like ChatGPT may not be able to translate the whole + file in one go; try to copy-paste by batch of 300 lines and ask the LLM to + `translate the values of the json above to (keep the keys unchanged)`. In order to fit the bottom + navigation bar on mobile, make sure the values for those keys are less than 10 characters: "nav.home", " + nav.messages", "nav.more", "nav.notifs", "nav.people". +- Duplicate the [fr](../web/public/md/fr) folder and rename it to the locale code (e.g., `de` for German). Translate all + the markdown files in the new folder. To do so, you can copy-paste each file into an LLM and ask it to + `translate the markdown above to `. + +That's all, no code needed! diff --git a/docs/knowledge.md b/docs/knowledge.md index a924f5ed..84c9bc3c 100644 --- a/docs/knowledge.md +++ b/docs/knowledge.md @@ -15,7 +15,7 @@ - express node api server `/backend/api` - one off scripts, like migrations `/backend/scripts` - supabase postgres. schema in `/backend/supabase` - - supabase-generated types in `/backend/supabase/schema.ts` + - supabase-generated types in `/common/src/supabase/schema.ts` - files shared between backend directories `/backend/shared` - anything in `/backend` can import from `shared`, but not vice versa - files shared between the frontend and backend in `/common` @@ -333,7 +333,7 @@ export const placeBet: APIHandler<'bet'> = async (props, auth) => { } ``` -And finally, you need to register the handler in `backend/api/src/routes.ts` +And finally, you need to register the handler in the `handlers` map in `backend/api/src/app.ts` ```ts import {placeBet} from './place-bet' @@ -454,7 +454,7 @@ const t = useT() t('common.key', 'English translations') ``` -Translations should go to the JSON files in `web/messages` (`de.json` and `fr.json`, as of now). +Translations should go to the JSON files in `common/messages` (`de.json` and `fr.json`, as of now; English is the inline fallback passed to `t()`). ### Misc coding tips diff --git a/docs/profile_fields.md b/docs/profile_fields.md new file mode 100644 index 00000000..4918af23 --- /dev/null +++ b/docs/profile_fields.md @@ -0,0 +1,49 @@ +# Adding a new profile field + +A profile field is any variable associated with a user profile, such as age, politics, diet, etc. You may want to add a +new profile field if it helps people find better matches. + +To do so, you add code here: + +- common/src/supabase/schema.ts +- web/components/filters/choices.tsx (if multi choices) +- web/components/optional-profile-form.tsx +- web/components/profile-about.tsx +- backend/api/src/get-profiles.ts +- common/src/api/schema.ts ('get-profiles' props) +- common/src/api/zod-types.ts (optionalProfilesSchema) +- web/components/filters/filters.tsx +- common/src/filters.ts +- web/components/filters/use-filters.ts (yourFilters and isYourFilters) + +Note that you will also need to add a column to the `profiles` table; you +can do so via this SQL command (change the type and index if not `TEXT`): + +```sql +ALTER TABLE profiles +ADD COLUMN profile_field TEXT; + +CREATE INDEX IF NOT EXISTS idx_profiles_profile_field ON profiles USING btree (mbti); +``` + +Store it in `add_.sql` in the [migrations](../backend/supabase/migrations) folder and +run [migrate.sh](../scripts/migrate.sh) from the root folder: + +```bash +./scripts/migrate.sh backend/supabase/migrations/add_.sql +``` + +Optionally, if you use the remote dev DB, run the SQL above in the dev DB and sync the database types from supabase to +the local files (which assist Typescript in typing): + +```bash +yarn --cwd=backend/api regen-types-dev +``` + +If you use your local DB, load the new schema with: + +```bash +yarn test:db:reset +``` + +That's it!