Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,17 @@ Requires Node 22+, pnpm 11.5.0, the Supabase CLI, and Docker.
- **Routing:** App Router under `src/app`. Route groups: `(app)` is the authenticated surface (its layout guards on `supabase.auth.getUser()` and redirects to `/login`); `(auth)` holds `login`, `signup`, and `invite/[token]`. `auth/callback/route.ts` exchanges the auth code for a session and accepts invites.
- **Session handling:** `src/proxy.ts` (Next.js 16 proxy, formerly `middleware.ts`) calls `updateSession` to refresh the Supabase session cookie on each request. Keep the `matcher` in sync if route patterns change.
- **Supabase clients (`src/lib/supabase`):** use `createSupabaseServerClient()` in Server Components / route handlers, `createSupabaseBrowserClient()` in Client Components, and the `middleware` helper for session refresh. All env access goes through `getSupabaseEnv()` - do not read `process.env` for Supabase keys directly.
- **Features:** Feature-scoped code lives in `src/features/<feature>`. The main UI is `features/chat/chat-workspace.tsx`. Message list state uses the pure helpers in `features/messages/optimistic.ts` (`mergeIncomingMessage`, `markMessageFailed`, `removeMessage`) - keep these pure and unit-tested.
- **Features:** Feature-scoped code lives in `src/features/<feature>`. The chat orchestrator is `features/chat/chat-workspace.tsx`; to stay under `max-lines` it delegates to focused units: presentational pieces in `chat-parts.tsx`, the `WorkspaceSidebar`, the `useChannelRealtime` / `useUnread` / `useNotifications` hooks, and pure helpers (`unread.ts`, `notifications.ts`, `status.ts`, `emoji-recents.ts`, `unfurl.ts`). Message list state uses the pure helpers in `features/messages/optimistic.ts` (`mergeIncomingMessage`, `markMessageFailed`, `removeMessage`) - keep all such helpers pure and unit-tested.
- **Slack-parity features:** message edit/soft-delete, full emoji reactions (frimousse picker + localStorage recents), markdown rendering (`message-body.tsx`, sanitized via `rehype-sanitize`), unread badges/divider (`channel_unread_counts` RPC), the notification bell over the `user:<id>` realtime topic, custom status/presence (`status-control.tsx`), and SSRF-safe link unfurling (`/api/unfurl` + `link_previews`).
- **Realtime + optimistic UI:** Messages are sent optimistically (marked `pending`), then reconciled when the realtime broadcast echoes back (deduped by id). Preserve this pattern; do not block the UI on the network round-trip.
- **Types:** UI-facing domain types live in `src/types/chat.ts`; the Supabase schema type is `src/types/database.ts` (`Database`). Supabase clients are typed with `Database`.

## Database

- Schema, RLS policies, functions, and triggers live in `supabase/migrations/`. Never edit an applied migration - add a new sequentially numbered migration file (e.g. `003_*.sql`).
- All tables are protected by **row-level security** and scoped per organization via `current_org_id()`. Any new table must enable RLS and define policies; otherwise data is inaccessible to clients.
- Tables: `organizations`, `profiles`, `channels`, `channel_members`, `messages`, `reactions`, `attachments`, `mentions`, `notifications`, `invites`.
- Server-side logic prefers Postgres functions/RPCs (`create_invite`, `accept_invite`, `search_messages`) over ad-hoc client queries for privileged operations.
- Tables: `organizations`, `profiles`, `channels`, `channel_members`, `messages`, `reactions`, `attachments`, `mentions`, `notifications`, `invites`, `link_previews`.
- Server-side logic prefers Postgres functions/RPCs (`create_invite`, `accept_invite`, `search_messages`, `channel_unread_counts`) over ad-hoc client queries for privileged operations. Notification triggers (mention/thread/dm/reaction) broadcast to each recipient's `user:<id>` private realtime topic, which is read-gated by an RLS policy on `realtime.messages`.
- After changing the schema, update `src/types/database.ts` to match, and run `supabase db reset` locally to verify migrations apply cleanly.
- For the full migration + RLS + types workflow, follow the `supabase-migration` skill in [`.factory/skills/supabase-migration/SKILL.md`](./.factory/skills/supabase-migration/SKILL.md).

Expand All @@ -72,7 +73,7 @@ Requires Node 22+, pnpm 11.5.0, the Supabase CLI, and Docker.
## Testing

- **Unit tests (Vitest):** jsdom environment with globals enabled. Test files are `*.test.ts` / `*.test.tsx` colocated with source under `src/` (enforced by the `include` glob in `vitest.config.ts`). Favor fast, pure unit tests for logic (see `features/messages/optimistic.test.ts`); keep business logic in pure functions so it is testable without Supabase or the DOM. Run `pnpm test`.
- **Coverage thresholds:** `pnpm test:coverage` enforces coverage on the pure logic layer (`src/lib/utils.ts`, `src/features/messages/optimistic.ts`): 100% statements/functions/lines and 90% branches (see `vitest.config.ts`). When you add logic to those modules or extract new pure helpers, add unit tests so the gate stays green. CI runs `pnpm test:ci`, which also fails on a coverage drop.
- **Coverage thresholds:** `pnpm test:coverage` enforces coverage on the pure logic layer (the `include` list in `vitest.config.ts`: `src/lib/utils.ts`, `src/features/messages/optimistic.ts`, and the chat helpers `unread.ts`, `notifications.ts`, `status.ts`, `emoji-recents.ts`, `unfurl.ts`): 100% statements/functions/lines and 90% branches. When you add logic to those modules or extract new pure helpers, add unit tests so the gate stays green. CI runs `pnpm test:ci`, which also fails on a coverage drop.
- **Test performance + flaky detection:** `pnpm test:ci` writes a JUnit report to `reports/junit.xml` (per-suite timing) and is uploaded as a CI artifact; Vitest flags tests slower than `slowTestThreshold` (300ms). Vitest retries once locally / twice in CI and Playwright retries twice in CI, so a test that only passes on retry is surfaced as flaky in the JUnit/HTML reports rather than silently masked.
- **End-to-end tests (Playwright):** specs live in `e2e/*.spec.ts` and are configured in `playwright.config.ts`. The config builds and starts the Next.js production server (`pnpm build && pnpm start`) with dummy `NEXT_PUBLIC_SUPABASE_*` env vars, so tests run against the shipped artifact and the public auth pages plus the auth-guard redirect can be tested without a running Supabase backend. Run `pnpm test:e2e` (or `pnpm test:e2e:ui` for the inspector). E2E specs are excluded from Vitest, and Vitest unit tests are excluded from Playwright.
- E2E tests that depend on an authenticated session require a running local Supabase stack (`supabase start`) and real credentials; keep those separate from the no-backend smoke tests.
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,15 @@ Low-latency team chat for a single organization. Flack is a real-time messaging

- Public and private channels, plus direct messages (`channels.type`: `public` | `private` | `dm`)
- Threaded replies (`messages.parent_id`)
- Message editing and soft-delete with realtime reconciliation
- Optimistic message sending with realtime broadcast reconciliation
- Rich text rendering (sanitized GitHub-flavored markdown)
- Full emoji reactions via picker with recent-emoji history
- Mentions with notifications, reactions, and file attachments (Supabase Storage)
- Unread tracking: per-channel badges (`channel_unread_counts` RPC), new-messages divider, mark-on-read
- Notification center with a live bell over the `user:<id>` private realtime topic
- Custom status (emoji/text/expiry) and presence (active/away/do-not-disturb); do-not-disturb suppresses activity toasts
- SSRF-safe link unfurling (`/api/unfurl`) with `link_previews` caching
- Full-text message search via the `search_messages` Postgres function
- Multi-tenant organizations with row-level security (RLS) isolating data per org
- Invite-based onboarding (`create_invite` / `accept_invite` RPCs)
Expand Down Expand Up @@ -132,9 +139,9 @@ playwright.config.ts # Playwright configuration

Schema lives in `supabase/migrations/`. Core tables (all with RLS, scoped per organization):

`organizations`, `profiles`, `channels`, `channel_members`, `messages`, `reactions`, `attachments`, `mentions`, `notifications`, `invites`.
`organizations`, `profiles`, `channels`, `channel_members`, `messages`, `reactions`, `attachments`, `mentions`, `notifications`, `invites`, `link_previews`.

Key database functions: `current_org_id()`, `is_admin()`, `is_channel_member()`, `create_invite()`, `accept_invite()`, `search_messages()`. Triggers broadcast message/reaction changes over Supabase Realtime and create mention notifications. A `handle_new_user` trigger provisions a profile when an auth user is created.
Key database functions: `current_org_id()`, `is_admin()`, `is_channel_member()`, `create_invite()`, `accept_invite()`, `search_messages()`, `channel_unread_counts()`. Triggers broadcast message/reaction changes over Supabase Realtime, create mention/thread/dm/reaction notifications, and broadcast new notifications to each recipient's `user:<id>` private topic. A `handle_new_user` trigger provisions a profile when an auth user is created.

## Testing

Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,15 @@
"@tailwindcss/postcss": "^4.1.17",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"frimousse": "^0.3.0",
"lucide-react": "^0.555.0",
"next": "^16.2.9",
"pino": "^10.3.1",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-markdown": "^10.1.0",
"rehype-sanitize": "^6.0.0",
"remark-gfm": "^4.0.1",
"tailwind-merge": "^3.6.0"
},
"devDependencies": {
Expand Down
Loading
Loading