{"owner":"lukevella","repo":"rallly","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["CLAUDE.md",".cursorrules"],"skills":{"CLAUDE.md":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Project Overview\n\nRallly is a meeting scheduling application built with Next.js that allows users to create polls to find the best meeting times. It supports both self-hosted and cloud-hosted deployments with a freemium model.\n\n**Core Technologies:**\n- Next.js 16 with React 19\n- tRPC for API layer\n- Prisma with PostgreSQL\n- Better-Auth for authentication\n- TailwindCSS for styling\n- TypeScript throughout\n- dayjs for date handling\n\n## Development Commands\n\n### Getting Started\n```bash\n# Install dependencies\npnpm install\n\n# Setup environment\ncp apps/web/.env.sample apps/web/.env\ncp packages/database/.env.sample packages/database/.env\n\n# Generate Prisma client\npnpm db:generate\n\n# Start development database\npnpm docker:up\n\n# Reset/setup database with seed data\npnpm db:reset\n\n# Start development server\npnpm dev\n```\n\n### Common Commands\n```bash\n# Development\npnpm dev                    # Start web app dev server\npnpm dev:landing           # Start landing page dev server  \npnpm dev:emails            # Start email template dev server\n\n# Building\npnpm build                 # Build web app\npnpm build:web            # Build web app with version injection\npnpm build:landing        # Build landing page\npnpm build:test           # Build for testing\n\n# Database\npnpm db:migrate           # Run database migrations\npnpm db:push              # Push schema changes\npnpm db:deploy            # Deploy migrations (production)\n\n# Testing\npnpm test:unit            # Run unit tests (Vitest)\npnpm test:integration     # Run integration tests (Playwright)\n\n# Code Quality\npnpm check                # Run Biome linter/formatter\npnpm check:fix            # Auto-fix linting issues\npnpm type-check           # Run TypeScript type checking\n\n# Utilities\npnpm i18n:scan            # Scan for translation keys\npnpm sherif               # Check package dependencies\n```\n\n## Architecture\n\n### Monorepo Structure\n- `apps/web/` - Main Next.js application\n- `apps/landing/` - Marketing/landing page\n- `apps/docs/` - Documentation site\n- `packages/` - Shared packages:\n  - `database/` - Prisma schema and client\n  - `ui/` - Shared UI components\n  - `emails/` - Email templates\n  - `posthog/` - Analytics client\n  - `billing/` - Stripe integration\n  - `utils/` - Shared utilities\n\n### Key Features & Structure\n- **Polls**: Core scheduling functionality in `apps/web/src/features/poll/`\n- **Spaces**: Workspace/team organization in `apps/web/src/features/space/`\n- **Authentication**: Better-Auth config in `apps/web/src/lib/auth.ts`, domain logic in `apps/web/src/features/auth/`\n- **tRPC API**: Routers in `apps/web/src/trpc/routers/`\n- **Feature Flags**: Quick create and other toggles in `apps/web/src/lib/feature-flags/`\n\n### Database\n- PostgreSQL with Prisma ORM\n- Multi-model schema split across files in `packages/database/prisma/models/`\n- Supports both cloud (Vercel KV) and self-hosted Redis for rate limiting\n\n### Authentication & Authorization\n- Better-Auth with multiple providers (Google, Microsoft, OIDC, email OTP, guest)\n- CASL-based permissions system for spaces and polls\n- User roles: admin, member with tier-based abilities (free/pro)\n\n### Deployment Modes\n- **Cloud Hosted**: Full SaaS with Stripe billing on Vercel\n- **Self Hosted**: Docker-based deployment without billing features\n- Environment variable `NEXT_PUBLIC_SELF_HOSTED=true` toggles features\n\n## Testing\n\n### Integration Tests (Playwright)\n- Located in `apps/web/tests/`\n- Use `pnpm test:integration` to run\n- Test utilities in `apps/web/tests/test-utils.ts`\n- Mailpit integration for email testing\n\n### Unit Tests (Vitest)\n- Use `pnpm test:unit` to run\n- Test files co-located with source code\n\n## Git Commits\nAlways use gitmoji prefixes in commit messages. Follow the gitmoji convention (https://gitmoji.dev) for the type of change (e.g. `📝` docs, `♻️` refactor, `🐛` fix, `✨` feature).\n\n## Code Standards\n\n### Styling\n- TailwindCSS with custom design system\n- Use `cn()` from `@rallly/ui` to compose classes\n- Biome for code formatting (indent: 2 spaces, double quotes)\n- Custom UI components in `packages/ui/src/` (shadcn-ui components go here)\n\n### UI Copy\n- **Sentence case** for all UI copy: capitalize the first word and proper nouns only. Applies to buttons, dialog titles, page and section headings, setting row titles, empty states, form labels, menu items and toast titles.\n- Exceptions — the test is **\"does the string name a thing?\"**, not \"is it a heading?\":\n  - Proper nouns and brands: \"Google Calendar\", \"Microsoft Calendar\"\n  - Initialisms stay uppercase, the rest lowercases: \"API keys\", \"Download ICS file\"\n  - Product feature names: \"Event Types\", \"Quick Create\", \"Control Panel\"\n  - Sample/placeholder data standing in for user content: \"Jessie Smith\", \"My Team\"\n- **Headings and dialog titles are not exempt.** Title Case has no single agreed rule (Chicago, AP and APA disagree), so it cannot be applied consistently; it degrades screen reader pronunciation and removes word-shape cues used by readers with dyslexia and low vision; and the same string often serves as both a button label and a dialog title, so a position-based rule would force two strings for one concept.\n- **Changing `defaults` alone is not enough.** `pnpm i18n:scan` will not overwrite an existing key's value — the UI keeps rendering the old copy. Run `pnpm i18n:sync` (`--sync-primary`) to push changed English values through. Never run `--sync-all`; it clears other locales' translations.\n\n### State Management\n- tRPC with TanStack Query for server state\n- React Context for client state (auth, preferences, etc.)\n- Form state with react-hook-form + Zod validation\n\n### TypeScript Conventions\n- Prefer inline prop types over named interfaces for simple component props\n- Example: `function Component({ prop }: { prop: string })` instead of defining a separate interface\n- Only create named interfaces when they're reused or complex\n- Create separate import statements for types (use `import type`)\n- Prefer `React.useState`, `React.useEffect`, etc. over standalone imports (`useState`, `useEffect`)\n- Prefer implicit over explicit return types\n\n### Dialog Management\n- **IMPORTANT**: Always use the `useDialog` hook from `@rallly/ui/dialog` for managing dialog state instead of manual `useState` for open/close state\n- The hook provides `dialog.trigger()`, `dialog.dismiss()`, and `dialog.dialogProps` which should be spread onto the dialog component\n- Example usage:\n  ```tsx\n  const dialog = useDialog();\n  \n  // Trigger dialog\n  <Button onClick={() => dialog.trigger()}>Open</Button>\n  \n  // Dialog component\n  <MyDialog {...dialog.dialogProps} />\n  ```\n\n### Component Conventions\n- Prefer composable components in the style of shadcn UI over large monolithic components\n- Keep component props minimal — pass only the bare minimum information needed\n- Add `\"use client\"` directive to the top of any `.tsx` file that requires client-side JavaScript\n\n### Streaming & Server Data Access\nThese rules prepare the app for Next.js `cacheComponents` (static shell + streamed dynamic content). Follow them in all new and touched server code:\n- Never await runtime data (session, `cookies()`, `headers()`, tRPC prefetches) at the top of a layout or page. Extract the awaits into a private async component rendered under a `<Suspense>` boundary (see the gate pattern in `app/[locale]/(space)/layout.tsx`) so the shell streams immediately.\n- Pass `params`/`searchParams` promises down into Suspense-wrapped children and await them there, instead of awaiting at the top of the page.\n- Don't call argless `dayjs()`, `new Date()`, `Date.now()`, or `Math.random()` during server render outside request-bound components — synchronous IO fails prerendering once `cacheComponents` is enabled. Compute \"now\" on the client or behind a Suspense boundary after `connection()`.\n\n### Clock-Classified Reads (upcoming/past/relative-to-now)\nReads filtered or grouped by the viewer's present (\"upcoming\", \"past\", agenda groupings) have no cacheable answer: the classification depends on the viewer's clock and timezone. All-day events are floating calendar dates (RFC 5545), so classifying them requires the viewer's current calendar date — never drop the timezone from such a query; that silently substitutes UTC as the viewer's zone. Choose the transport by what the data is to the surface:\n- **Content of a long-lived surface** (events list, agenda/calendar): fetch on the client — server renders the shell, the client queries with `getBrowserTimeZone()` and revalidates (refetch on focus). This gives the fastest paint and keeps the data fresh while the page stays open.\n- **Passing annotation on navigation chrome** (a count badge on a tile): a server snapshot is correct. Resolve the zone device-cookie-first via `getDeviceTimeZone()` from `@/lib/datetime/server` (falls back to the stored `user.timeZone`, then UTC). The session zone override is a poll-viewing aid and must not affect classification; `getDeviceDateTimeConfig` (which honors it) is for display on public pages.\n- **Never server-prefetch a client query with a zone the client will disagree with** (e.g. prefetching with the server's own zone and refetching after hydration). Seed `initialData` from the device cookie zone or render a skeleton until the client fetch lands.\n\n### File Organization\n- Route handlers follow Next.js App Router conventions\n- Always use kebab-case for file names\n\n### Directory Structure (apps/web/src)\n\n**Layering** — import direction is `app → features → components → lib`. Each layer may import from layers to its right, and anything may import `lib`. Nothing outside `app/` imports `@/app/*`. Boundaries are lint-enforced via `noRestrictedImports` in `apps/web/biome.json`; existing violations live on a shrinking migration allowlist there.\n\n- `app/` — routing only. Pages/routes are thin adapters composing `features/*`. A route-private `components/` folder is allowed for exactly one route segment; the moment a second segment needs a component, it moves to the owning feature.\n- `features/<domain>/` — the product (see below).\n- `components/` — shared, domain-agnostic UI only. Admission test: \"could this ship in a different product?\" Must not import from `features/` or `app/`. Cross-app reusables graduate to `packages/ui`.\n- `lib/` — infrastructure and cross-cutting clients (auth config, cache, datetime, errors, feature flags, rate limiting, storage). Never imports from `features/`, `components/`, or `app/`.\n\n**What is a feature** — a feature owns at least one of: a database entity, an external integration, or server-side lifecycle logic. UI-only folders are NOT features — they belong in `components/` or an owning feature's `components/`. Create the folder when the first server logic for a new domain noun appears — never speculatively, never for a component alone. Sub-concerns are subdirectories of their parent feature (e.g. `space/member/`), not sibling features.\n\n**Feature file vocabulary** — closed set; new file names require a team decision. Applies recursively in sub-concern directories:\n\n- `data.ts` — parameterized reads (Prisma queries), must start with `import \"server-only\"`. Trusts its input: every query takes explicit arguments and carries its tenant scope in the where clause. Never reads the request — no `next/headers`, `next/navigation`, or session state (lint enforced)\n- `loaders.ts` — request facing reads: resolve the actor from the session, apply page semantics (redirects to /login and /setup, `InvalidSessionError` on bans), and delegate to `data.ts` with proven scope. Loaders always consume `data.ts`, never the reverse, and never import `@rallly/database` — so query logic cannot fork between the session path and the API path. Server components call loaders; API routes, webhooks and cron call `data.ts` with proven scope from their own gate (API routes are lint banned from `@/features/**/loaders`). Naming note: `session-data.ts` and `queries.ts` were considered and rejected\n- `mutations.ts` — writes + cache invalidation, must start with `import \"server-only\"`. Trusts its input: takes explicit parameters (`userId`, not headers), no session reads, no `headers()`, callable from system contexts (webhooks, cron, moderation). Authorization happens in `actions.ts` (safe-action middleware + CASL against the database). Never call Better-Auth endpoints from a mutation — they resolve the target user from request headers and authorize against the session snapshot; use adapter-level APIs (`authLib.$context` → `internalAdapter`) instead\n- `actions.ts` — `\"use server\"` actions, thin wrappers over mutations, validated via safe-action + `schema.ts`. Owns authentication and authorization (safe-action clients + CASL checks against database state, never against the session snapshot). Exception to the thin-wrapper rule: writes whose target user is defined by the session (self-profile updates) call the Better-Auth endpoint directly in the action — the endpoint refreshes the session snapshot and cookie cache in one step. Rule of thumb: target user from a parameter → mutation via `internalAdapter`; target user from the session → Better-Auth endpoint in the action\n- `schema.ts` — Zod schemas (isomorphic, no Prisma imports)\n- `types.ts` — domain types\n- `ability.ts` — CASL permissions\n- `constants.ts`\n- `utils.ts` — pure domain helpers, co-located `*.test.ts`\n- `client.tsx` — client entry: providers, context, hooks, stores (no separate `hooks.ts` or store files)\n- `service.ts` — external integration client (class/factory wrapping a third-party API), must start with `import \"server-only\"`\n- `components/` — feature UI (no components at feature root)\n- `assets/` — static files used by the feature\n- NOT allowed: `index.ts` barrels, `helpers.ts`, `queries.ts`, `hooks.ts`, `lib/`, `libs/`\n\n**Read/write symmetry** — the request facing / core split is the same on both sides:\n\n|  | Request facing (session, redirects, auth) | Core (parameterized, trusts input) |\n| -- | -- | -- |\n| Reads | `loaders.ts` | `data.ts` |\n| Writes | `actions.ts` | `mutations.ts` |\n\n**Loader placement** — pages, layouts and route-private components never call `data.ts` directly: every read they consume goes through a loader in `features/<domain>/loaders.ts`, so the session gate that proves scope cannot be forgotten at the call site (lint-enforced: `@/features/**/data` is banned in `app/**`; existing violations sit on a shrinking migration allowlist in `apps/web/biome.json`). A loader bundles the gate and the read: `import \"server-only\"`, named with a `load*` prefix, wrapped in React `cache()` so every consumer in a request shares one read, resolves the actor via the session gates (`requireUser()`, `getActiveSpace()` — both trust the session cookie cache; `getCurrentUser()` is the database-verified read for pages that need DB-fresh user state) and delegates to `data.ts` with proven scope. Reference shape: `features/notifications/loaders.ts` (`loadNotificationPreferences`). Route-private `actions.ts` files are the write side, not pages — they keep calling `data.ts`/`mutations.ts` with proven scope from their safe-action gate (loaders' page semantics, redirects, don't belong in actions) and are exempt from the lint ban.\n\n**DAL enforcement** — `@rallly/database` may only be imported from `features/**/data.ts` and `features/**/mutations.ts` (lint-enforced via `noRestrictedImports`; existing violations sit on a shrinking migration allowlist in `apps/web/biome.json`; `loaders.ts` is absent from the allowlist, so database imports are banned there by default). Parameterized reads take their tenant scope as `spaceId: AuthorizedSpaceId` (from `@/features/space/types`); only the session gate (`createSpaceDTO`) and the API key middleware may cast to it. API routes under `app/api/**` (except tRPC and better-auth) must not import `@/features/**/loaders` — they authenticate their own way and pass proven scope to parameterized reads. The inverse holds for pages: `app/**` (outside `app/api/**` and route-private `actions.ts`) must not import `@/features/**/data` — reads reach pages only through loaders. `api/private` handlers own serialization and must parse response bodies through their zod schemas.\n\n**Cross-feature imports** — allowed, public surface only (the vocabulary files above); never reach into another feature's internals. No cycles between features (CI-enforced). For UI, prefer composing multiple features at the page level in `app/` over feature-to-feature component imports.\n\n**`trpc/` is frozen legacy transport** — queries only; routers call `features/*/data.ts`. No new mutations — writes are server actions calling `features/*/mutations.ts`.\n\n### PostHog Event Naming\n- Use the `category:object_action` pattern\n- Lowercase only, snake_case, present-tense verbs\n  - **category** — the context/flow (e.g. `poll_creation`, `account_settings`)\n  - **object** — the component/element (e.g. `invite_link`, `manage_button`)\n  - **action** — what happened (e.g. `click`, `copy`, `submit`)\n- Example: `posthog?.capture(\"poll_creation:manage_button_click\")`\n\n## i18n & Localization\n\n- i18next for internationalization\n- Translation files in `public/locales/[lang]/`\n- Crowdin integration for translation management\n- Use `pnpm i18n:scan` to extract new translation keys\n- **IMPORTANT**: When TypeScript errors occur for missing i18n keys, run `pnpm i18n:scan` instead of manually adding keys. This command automatically scans the codebase for `Trans` components and generates the necessary translation entries.\n- **IMPORTANT**: Never manually add translations to `.json` files. This is handled by tooling.\n- **Pluralization**: Always use ICU message format for plurals. Example: `{count, plural, =0 {No items} one {1 item} other {# items}}` instead of separate singular/plural translation keys.\n- i18n keys are in camelCase and should describe the message (e.g. `\"lastUpdated\": \"Last updated\"`)\n- If an i18n key is not intended to be reused, prefix it with the component name in camelCase\n- In client components, use the `<Trans>` component from `@/i18n/client` with the `defaults` prop:\n  ```tsx\n  import { Trans } from \"@/i18n/client\";\n  <Trans i18nKey=\"menu\" defaults=\"Menu\" />\n  ```\n- When a server component needs `t` or `i18n` itself — for a string outside JSX, or to render `TransWithoutContext` — get them from `getTranslation` in `@/i18n/server` (rendering the client `Trans` instead needs neither; see below):\n  ```tsx\n  import { Trans } from \"react-i18next/TransWithoutContext\";\n  import { getTranslation } from \"@/i18n/server\";\n\n  const { t, i18n } = await getTranslation();\n  t(\"menu\", { defaultValue: \"Menu\" });\n  <Trans t={t} i18n={i18n} ns=\"app\" i18nKey=\"menu\" defaults=\"Menu\" />;\n  ```\n  Whenever you use `TransWithoutContext`, pass **both** `t` and `i18n` — omitting `i18n` falls back to the module-global instance, which can carry another request's language under concurrency.\n- **Per-request instances are what make this safe.** `apps/web` (`i18n/client.tsx`, `initI18next`) and `packages/emails` (`createEmailI18n`) both build a fresh instance per render via `createInstance()`, so nothing is shared between concurrent requests. Any new i18n entry point must do the same — never `import i18next from \"i18next\"` (the package default export is a process-wide singleton) and never mutate a shared instance with `changeLanguage()` to serve a request. `apps/landing/src/i18n/` currently violates this and is the one place where passing `i18n` does *not* help, because the `i18n` it hands you **is** the singleton.\n- **Where each `Trans` is required**, and where it's a free choice:\n  - **Emails** — `TransWithoutContext` only, with `t` and `i18n` from `createEmailI18n(locale)` (`packages/emails/src/i18n.ts`). Emails are sent from server components, where React context is unavailable, and each render builds its own locale-bound instance so a concurrent fan-out can't cross languages. Only a Next build catches a violation.\n  - **`app/global-error.tsx`** — neither. It renders outside the `[locale]` segment that mounts `I18nProvider`, so there is no locale and no `t`. Copy there is hardcoded English.\n  - **Everywhere else** — either import is correct. `I18nProvider` is mounted in `app/[locale]/layout.tsx`, the root layout, so every page (public poll pages included) is inside it. A server component *may* render the `\"use client\"` `Trans` from `@/i18n/client`: React serializes it as a client reference and the browser renders it against the per-tab instance, so there is no cross-request language bleed and no extra bundle weight (`react-i18next` is already loaded app-wide). Prefer `TransWithoutContext` in server components to keep static copy out of hydration, but don't make an otherwise-static page `async` just to do it — and don't file the client import as a bug.",".cursorrules":"- Use pnpm for package management\n- Use dayjs for date handling\n- Use tailwindcss for styling\n- Use react-query for data fetching\n- Use react-hook-form for form handling\n- Prefer implicit return values over explicit return values\n- Use zod for form validation\n- Create separate import statements for types\n- All text in the UI should be translated using either the Trans component or the useTranslation hook\n- Prefer composable components in the style of shadcn UI over large monolithic components\n- DropdownMenuItem is a flex container with a preset gap so there is no need to add margins to the children\n- Keep the props of a component as minimal as possible. Pass only the bare minimum amount of information needed to it.\n- All text in the UI should be translatable.\n- i18n keys are in camelCase.\n- Use the <Trans> component in client components from @/i18n/client. Use the `defaults` prop to provide the default text. Example:\n\n```tsx\nimport { Trans } from \"@/i18n/client\";\n\n<Trans i18nKey=\"menu\" defaults=\"Menu\" />\n```\n\n- On the server use the `getTranslations` function from @/i18n/server to get the translations. Example:\n\n```ts\nconst { t } = await getTranslations();\n\nt(\"menu\", { defaultValue: \"Menu\" });\n```\n\n- shadcn-ui components should be added to packages/ui\n- Always use a composable patterns when building components\n- Use `cn()` from @rallly/ui to compose classes\n- Prefer using the React module APIs (e.g. React.useState) instead of standalone hooks (e.g. useState)\n- Do not attempt to fix typescript issues related to missing translations. This will be handled by our tooling.\n- Never manually add translations to .json files. This will be handled by our tooling.\n- Add the \"use client\" directive to the top of any .tsx file that requires client-side javascript\n- i18nKeys should describe the message in camelCase. Ex. \"lastUpdated\": \"Last Updated\"\n- If the i18nKey is not intended to be reused, prefix it with the component name in camelCase\n- Always use kebab-case for file names\n- Prefer double quotes for strings over single quotes\n- Only add comments when it is necessary to explain code that isn't self-explanatory\n"},"files":{"CLAUDE.md":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Project Overview\n\nRallly is a meeting scheduling application built with Next.js that allows users to create polls to find the best meeting times. It supports both self-hosted and cloud-hosted deployments with a freemium model.\n\n**Core Technologies:**\n- Next.js 16 with React 19\n- tRPC for API layer\n- Prisma with PostgreSQL\n- Better-Auth for authentication\n- TailwindCSS for styling\n- TypeScript throughout\n- dayjs for date handling\n\n## Development Commands\n\n### Getting Started\n```bash\n# Install dependencies\npnpm install\n\n# Setup environment\ncp apps/web/.env.sample apps/web/.env\ncp packages/database/.env.sample packages/database/.env\n\n# Generate Prisma client\npnpm db:generate\n\n# Start development database\npnpm docker:up\n\n# Reset/setup database with seed data\npnpm db:reset\n\n# Start development server\npnpm dev\n```\n\n### Common Commands\n```bash\n# Development\npnpm dev                    # Start web app dev server\npnpm dev:landing           # Start landing page dev server  \npnpm dev:emails            # Start email template dev server\n\n# Building\npnpm build                 # Build web app\npnpm build:web            # Build web app with version injection\npnpm build:landing        # Build landing page\npnpm build:test           # Build for testing\n\n# Database\npnpm db:migrate           # Run database migrations\npnpm db:push              # Push schema changes\npnpm db:deploy            # Deploy migrations (production)\n\n# Testing\npnpm test:unit            # Run unit tests (Vitest)\npnpm test:integration     # Run integration tests (Playwright)\n\n# Code Quality\npnpm check                # Run Biome linter/formatter\npnpm check:fix            # Auto-fix linting issues\npnpm type-check           # Run TypeScript type checking\n\n# Utilities\npnpm i18n:scan            # Scan for translation keys\npnpm sherif               # Check package dependencies\n```\n\n## Architecture\n\n### Monorepo Structure\n- `apps/web/` - Main Next.js application\n- `apps/landing/` - Marketing/landing page\n- `apps/docs/` - Documentation site\n- `packages/` - Shared packages:\n  - `database/` - Prisma schema and client\n  - `ui/` - Shared UI components\n  - `emails/` - Email templates\n  - `posthog/` - Analytics client\n  - `billing/` - Stripe integration\n  - `utils/` - Shared utilities\n\n### Key Features & Structure\n- **Polls**: Core scheduling functionality in `apps/web/src/features/poll/`\n- **Spaces**: Workspace/team organization in `apps/web/src/features/space/`\n- **Authentication**: Better-Auth config in `apps/web/src/lib/auth.ts`, domain logic in `apps/web/src/features/auth/`\n- **tRPC API**: Routers in `apps/web/src/trpc/routers/`\n- **Feature Flags**: Quick create and other toggles in `apps/web/src/lib/feature-flags/`\n\n### Database\n- PostgreSQL with Prisma ORM\n- Multi-model schema split across files in `packages/database/prisma/models/`\n- Supports both cloud (Vercel KV) and self-hosted Redis for rate limiting\n\n### Authentication & Authorization\n- Better-Auth with multiple providers (Google, Microsoft, OIDC, email OTP, guest)\n- CASL-based permissions system for spaces and polls\n- User roles: admin, member with tier-based abilities (free/pro)\n\n### Deployment Modes\n- **Cloud Hosted**: Full SaaS with Stripe billing on Vercel\n- **Self Hosted**: Docker-based deployment without billing features\n- Environment variable `NEXT_PUBLIC_SELF_HOSTED=true` toggles features\n\n## Testing\n\n### Integration Tests (Playwright)\n- Located in `apps/web/tests/`\n- Use `pnpm test:integration` to run\n- Test utilities in `apps/web/tests/test-utils.ts`\n- Mailpit integration for email testing\n\n### Unit Tests (Vitest)\n- Use `pnpm test:unit` to run\n- Test files co-located with source code\n\n## Git Commits\nAlways use gitmoji prefixes in commit messages. Follow the gitmoji convention (https://gitmoji.dev) for the type of change (e.g. `📝` docs, `♻️` refactor, `🐛` fix, `✨` feature).\n\n## Code Standards\n\n### Styling\n- TailwindCSS with custom design system\n- Use `cn()` from `@rallly/ui` to compose classes\n- Biome for code formatting (indent: 2 spaces, double quotes)\n- Custom UI components in `packages/ui/src/` (shadcn-ui components go here)\n\n### UI Copy\n- **Sentence case** for all UI copy: capitalize the first word and proper nouns only. Applies to buttons, dialog titles, page and section headings, setting row titles, empty states, form labels, menu items and toast titles.\n- Exceptions — the test is **\"does the string name a thing?\"**, not \"is it a heading?\":\n  - Proper nouns and brands: \"Google Calendar\", \"Microsoft Calendar\"\n  - Initialisms stay uppercase, the rest lowercases: \"API keys\", \"Download ICS file\"\n  - Product feature names: \"Event Types\", \"Quick Create\", \"Control Panel\"\n  - Sample/placeholder data standing in for user content: \"Jessie Smith\", \"My Team\"\n- **Headings and dialog titles are not exempt.** Title Case has no single agreed rule (Chicago, AP and APA disagree), so it cannot be applied consistently; it degrades screen reader pronunciation and removes word-shape cues used by readers with dyslexia and low vision; and the same string often serves as both a button label and a dialog title, so a position-based rule would force two strings for one concept.\n- **Changing `defaults` alone is not enough.** `pnpm i18n:scan` will not overwrite an existing key's value — the UI keeps rendering the old copy. Run `pnpm i18n:sync` (`--sync-primary`) to push changed English values through. Never run `--sync-all`; it clears other locales' translations.\n\n### State Management\n- tRPC with TanStack Query for server state\n- React Context for client state (auth, preferences, etc.)\n- Form state with react-hook-form + Zod validation\n\n### TypeScript Conventions\n- Prefer inline prop types over named interfaces for simple component props\n- Example: `function Component({ prop }: { prop: string })` instead of defining a separate interface\n- Only create named interfaces when they're reused or complex\n- Create separate import statements for types (use `import type`)\n- Prefer `React.useState`, `React.useEffect`, etc. over standalone imports (`useState`, `useEffect`)\n- Prefer implicit over explicit return types\n\n### Dialog Management\n- **IMPORTANT**: Always use the `useDialog` hook from `@rallly/ui/dialog` for managing dialog state instead of manual `useState` for open/close state\n- The hook provides `dialog.trigger()`, `dialog.dismiss()`, and `dialog.dialogProps` which should be spread onto the dialog component\n- Example usage:\n  ```tsx\n  const dialog = useDialog();\n  \n  // Trigger dialog\n  <Button onClick={() => dialog.trigger()}>Open</Button>\n  \n  // Dialog component\n  <MyDialog {...dialog.dialogProps} />\n  ```\n\n### Component Conventions\n- Prefer composable components in the style of shadcn UI over large monolithic components\n- Keep component props minimal — pass only the bare minimum information needed\n- Add `\"use client\"` directive to the top of any `.tsx` file that requires client-side JavaScript\n\n### Streaming & Server Data Access\nThese rules prepare the app for Next.js `cacheComponents` (static shell + streamed dynamic content). Follow them in all new and touched server code:\n- Never await runtime data (session, `cookies()`, `headers()`, tRPC prefetches) at the top of a layout or page. Extract the awaits into a private async component rendered under a `<Suspense>` boundary (see the gate pattern in `app/[locale]/(space)/layout.tsx`) so the shell streams immediately.\n- Pass `params`/`searchParams` promises down into Suspense-wrapped children and await them there, instead of awaiting at the top of the page.\n- Don't call argless `dayjs()`, `new Date()`, `Date.now()`, or `Math.random()` during server render outside request-bound components — synchronous IO fails prerendering once `cacheComponents` is enabled. Compute \"now\" on the client or behind a Suspense boundary after `connection()`.\n\n### Clock-Classified Reads (upcoming/past/relative-to-now)\nReads filtered or grouped by the viewer's present (\"upcoming\", \"past\", agenda groupings) have no cacheable answer: the classification depends on the viewer's clock and timezone. All-day events are floating calendar dates (RFC 5545), so classifying them requires the viewer's current calendar date — never drop the timezone from such a query; that silently substitutes UTC as the viewer's zone. Choose the transport by what the data is to the surface:\n- **Content of a long-lived surface** (events list, agenda/calendar): fetch on the client — server renders the shell, the client queries with `getBrowserTimeZone()` and revalidates (refetch on focus). This gives the fastest paint and keeps the data fresh while the page stays open.\n- **Passing annotation on navigation chrome** (a count badge on a tile): a server snapshot is correct. Resolve the zone device-cookie-first via `getDeviceTimeZone()` from `@/lib/datetime/server` (falls back to the stored `user.timeZone`, then UTC). The session zone override is a poll-viewing aid and must not affect classification; `getDeviceDateTimeConfig` (which honors it) is for display on public pages.\n- **Never server-prefetch a client query with a zone the client will disagree with** (e.g. prefetching with the server's own zone and refetching after hydration). Seed `initialData` from the device cookie zone or render a skeleton until the client fetch lands.\n\n### File Organization\n- Route handlers follow Next.js App Router conventions\n- Always use kebab-case for file names\n\n### Directory Structure (apps/web/src)\n\n**Layering** — import direction is `app → features → components → lib`. Each layer may import from layers to its right, and anything may import `lib`. Nothing outside `app/` imports `@/app/*`. Boundaries are lint-enforced via `noRestrictedImports` in `apps/web/biome.json`; existing violations live on a shrinking migration allowlist there.\n\n- `app/` — routing only. Pages/routes are thin adapters composing `features/*`. A route-private `components/` folder is allowed for exactly one route segment; the moment a second segment needs a component, it moves to the owning feature.\n- `features/<domain>/` — the product (see below).\n- `components/` — shared, domain-agnostic UI only. Admission test: \"could this ship in a different product?\" Must not import from `features/` or `app/`. Cross-app reusables graduate to `packages/ui`.\n- `lib/` — infrastructure and cross-cutting clients (auth config, cache, datetime, errors, feature flags, rate limiting, storage). Never imports from `features/`, `components/`, or `app/`.\n\n**What is a feature** — a feature owns at least one of: a database entity, an external integration, or server-side lifecycle logic. UI-only folders are NOT features — they belong in `components/` or an owning feature's `components/`. Create the folder when the first server logic for a new domain noun appears — never speculatively, never for a component alone. Sub-concerns are subdirectories of their parent feature (e.g. `space/member/`), not sibling features.\n\n**Feature file vocabulary** — closed set; new file names require a team decision. Applies recursively in sub-concern directories:\n\n- `data.ts` — parameterized reads (Prisma queries), must start with `import \"server-only\"`. Trusts its input: every query takes explicit arguments and carries its tenant scope in the where clause. Never reads the request — no `next/headers`, `next/navigation`, or session state (lint enforced)\n- `loaders.ts` — request facing reads: resolve the actor from the session, apply page semantics (redirects to /login and /setup, `InvalidSessionError` on bans), and delegate to `data.ts` with proven scope. Loaders always consume `data.ts`, never the reverse, and never import `@rallly/database` — so query logic cannot fork between the session path and the API path. Server components call loaders; API routes, webhooks and cron call `data.ts` with proven scope from their own gate (API routes are lint banned from `@/features/**/loaders`). Naming note: `session-data.ts` and `queries.ts` were considered and rejected\n- `mutations.ts` — writes + cache invalidation, must start with `import \"server-only\"`. Trusts its input: takes explicit parameters (`userId`, not headers), no session reads, no `headers()`, callable from system contexts (webhooks, cron, moderation). Authorization happens in `actions.ts` (safe-action middleware + CASL against the database). Never call Better-Auth endpoints from a mutation — they resolve the target user from request headers and authorize against the session snapshot; use adapter-level APIs (`authLib.$context` → `internalAdapter`) instead\n- `actions.ts` — `\"use server\"` actions, thin wrappers over mutations, validated via safe-action + `schema.ts`. Owns authentication and authorization (safe-action clients + CASL checks against database state, never against the session snapshot). Exception to the thin-wrapper rule: writes whose target user is defined by the session (self-profile updates) call the Better-Auth endpoint directly in the action — the endpoint refreshes the session snapshot and cookie cache in one step. Rule of thumb: target user from a parameter → mutation via `internalAdapter`; target user from the session → Better-Auth endpoint in the action\n- `schema.ts` — Zod schemas (isomorphic, no Prisma imports)\n- `types.ts` — domain types\n- `ability.ts` — CASL permissions\n- `constants.ts`\n- `utils.ts` — pure domain helpers, co-located `*.test.ts`\n- `client.tsx` — client entry: providers, context, hooks, stores (no separate `hooks.ts` or store files)\n- `service.ts` — external integration client (class/factory wrapping a third-party API), must start with `import \"server-only\"`\n- `components/` — feature UI (no components at feature root)\n- `assets/` — static files used by the feature\n- NOT allowed: `index.ts` barrels, `helpers.ts`, `queries.ts`, `hooks.ts`, `lib/`, `libs/`\n\n**Read/write symmetry** — the request facing / core split is the same on both sides:\n\n|  | Request facing (session, redirects, auth) | Core (parameterized, trusts input) |\n| -- | -- | -- |\n| Reads | `loaders.ts` | `data.ts` |\n| Writes | `actions.ts` | `mutations.ts` |\n\n**Loader placement** — pages, layouts and route-private components never call `data.ts` directly: every read they consume goes through a loader in `features/<domain>/loaders.ts`, so the session gate that proves scope cannot be forgotten at the call site (lint-enforced: `@/features/**/data` is banned in `app/**`; existing violations sit on a shrinking migration allowlist in `apps/web/biome.json`). A loader bundles the gate and the read: `import \"server-only\"`, named with a `load*` prefix, wrapped in React `cache()` so every consumer in a request shares one read, resolves the actor via the session gates (`requireUser()`, `getActiveSpace()` — both trust the session cookie cache; `getCurrentUser()` is the database-verified read for pages that need DB-fresh user state) and delegates to `data.ts` with proven scope. Reference shape: `features/notifications/loaders.ts` (`loadNotificationPreferences`). Route-private `actions.ts` files are the write side, not pages — they keep calling `data.ts`/`mutations.ts` with proven scope from their safe-action gate (loaders' page semantics, redirects, don't belong in actions) and are exempt from the lint ban.\n\n**DAL enforcement** — `@rallly/database` may only be imported from `features/**/data.ts` and `features/**/mutations.ts` (lint-enforced via `noRestrictedImports`; existing violations sit on a shrinking migration allowlist in `apps/web/biome.json`; `loaders.ts` is absent from the allowlist, so database imports are banned there by default). Parameterized reads take their tenant scope as `spaceId: AuthorizedSpaceId` (from `@/features/space/types`); only the session gate (`createSpaceDTO`) and the API key middleware may cast to it. API routes under `app/api/**` (except tRPC and better-auth) must not import `@/features/**/loaders` — they authenticate their own way and pass proven scope to parameterized reads. The inverse holds for pages: `app/**` (outside `app/api/**` and route-private `actions.ts`) must not import `@/features/**/data` — reads reach pages only through loaders. `api/private` handlers own serialization and must parse response bodies through their zod schemas.\n\n**Cross-feature imports** — allowed, public surface only (the vocabulary files above); never reach into another feature's internals. No cycles between features (CI-enforced). For UI, prefer composing multiple features at the page level in `app/` over feature-to-feature component imports.\n\n**`trpc/` is frozen legacy transport** — queries only; routers call `features/*/data.ts`. No new mutations — writes are server actions calling `features/*/mutations.ts`.\n\n### PostHog Event Naming\n- Use the `category:object_action` pattern\n- Lowercase only, snake_case, present-tense verbs\n  - **category** — the context/flow (e.g. `poll_creation`, `account_settings`)\n  - **object** — the component/element (e.g. `invite_link`, `manage_button`)\n  - **action** — what happened (e.g. `click`, `copy`, `submit`)\n- Example: `posthog?.capture(\"poll_creation:manage_button_click\")`\n\n## i18n & Localization\n\n- i18next for internationalization\n- Translation files in `public/locales/[lang]/`\n- Crowdin integration for translation management\n- Use `pnpm i18n:scan` to extract new translation keys\n- **IMPORTANT**: When TypeScript errors occur for missing i18n keys, run `pnpm i18n:scan` instead of manually adding keys. This command automatically scans the codebase for `Trans` components and generates the necessary translation entries.\n- **IMPORTANT**: Never manually add translations to `.json` files. This is handled by tooling.\n- **Pluralization**: Always use ICU message format for plurals. Example: `{count, plural, =0 {No items} one {1 item} other {# items}}` instead of separate singular/plural translation keys.\n- i18n keys are in camelCase and should describe the message (e.g. `\"lastUpdated\": \"Last updated\"`)\n- If an i18n key is not intended to be reused, prefix it with the component name in camelCase\n- In client components, use the `<Trans>` component from `@/i18n/client` with the `defaults` prop:\n  ```tsx\n  import { Trans } from \"@/i18n/client\";\n  <Trans i18nKey=\"menu\" defaults=\"Menu\" />\n  ```\n- When a server component needs `t` or `i18n` itself — for a string outside JSX, or to render `TransWithoutContext` — get them from `getTranslation` in `@/i18n/server` (rendering the client `Trans` instead needs neither; see below):\n  ```tsx\n  import { Trans } from \"react-i18next/TransWithoutContext\";\n  import { getTranslation } from \"@/i18n/server\";\n\n  const { t, i18n } = await getTranslation();\n  t(\"menu\", { defaultValue: \"Menu\" });\n  <Trans t={t} i18n={i18n} ns=\"app\" i18nKey=\"menu\" defaults=\"Menu\" />;\n  ```\n  Whenever you use `TransWithoutContext`, pass **both** `t` and `i18n` — omitting `i18n` falls back to the module-global instance, which can carry another request's language under concurrency.\n- **Per-request instances are what make this safe.** `apps/web` (`i18n/client.tsx`, `initI18next`) and `packages/emails` (`createEmailI18n`) both build a fresh instance per render via `createInstance()`, so nothing is shared between concurrent requests. Any new i18n entry point must do the same — never `import i18next from \"i18next\"` (the package default export is a process-wide singleton) and never mutate a shared instance with `changeLanguage()` to serve a request. `apps/landing/src/i18n/` currently violates this and is the one place where passing `i18n` does *not* help, because the `i18n` it hands you **is** the singleton.\n- **Where each `Trans` is required**, and where it's a free choice:\n  - **Emails** — `TransWithoutContext` only, with `t` and `i18n` from `createEmailI18n(locale)` (`packages/emails/src/i18n.ts`). Emails are sent from server components, where React context is unavailable, and each render builds its own locale-bound instance so a concurrent fan-out can't cross languages. Only a Next build catches a violation.\n  - **`app/global-error.tsx`** — neither. It renders outside the `[locale]` segment that mounts `I18nProvider`, so there is no locale and no `t`. Copy there is hardcoded English.\n  - **Everywhere else** — either import is correct. `I18nProvider` is mounted in `app/[locale]/layout.tsx`, the root layout, so every page (public poll pages included) is inside it. A server component *may* render the `\"use client\"` `Trans` from `@/i18n/client`: React serializes it as a client reference and the browser renders it against the per-tab instance, so there is no cross-request language bleed and no extra bundle weight (`react-i18next` is already loaded app-wide). Prefer `TransWithoutContext` in server components to keep static copy out of hydration, but don't make an otherwise-static page `async` just to do it — and don't file the client import as a bug.",".cursorrules":"- Use pnpm for package management\n- Use dayjs for date handling\n- Use tailwindcss for styling\n- Use react-query for data fetching\n- Use react-hook-form for form handling\n- Prefer implicit return values over explicit return values\n- Use zod for form validation\n- Create separate import statements for types\n- All text in the UI should be translated using either the Trans component or the useTranslation hook\n- Prefer composable components in the style of shadcn UI over large monolithic components\n- DropdownMenuItem is a flex container with a preset gap so there is no need to add margins to the children\n- Keep the props of a component as minimal as possible. Pass only the bare minimum amount of information needed to it.\n- All text in the UI should be translatable.\n- i18n keys are in camelCase.\n- Use the <Trans> component in client components from @/i18n/client. Use the `defaults` prop to provide the default text. Example:\n\n```tsx\nimport { Trans } from \"@/i18n/client\";\n\n<Trans i18nKey=\"menu\" defaults=\"Menu\" />\n```\n\n- On the server use the `getTranslations` function from @/i18n/server to get the translations. Example:\n\n```ts\nconst { t } = await getTranslations();\n\nt(\"menu\", { defaultValue: \"Menu\" });\n```\n\n- shadcn-ui components should be added to packages/ui\n- Always use a composable patterns when building components\n- Use `cn()` from @rallly/ui to compose classes\n- Prefer using the React module APIs (e.g. React.useState) instead of standalone hooks (e.g. useState)\n- Do not attempt to fix typescript issues related to missing translations. This will be handled by our tooling.\n- Never manually add translations to .json files. This will be handled by our tooling.\n- Add the \"use client\" directive to the top of any .tsx file that requires client-side javascript\n- i18nKeys should describe the message in camelCase. Ex. \"lastUpdated\": \"Last Updated\"\n- If the i18nKey is not intended to be reused, prefix it with the component name in camelCase\n- Always use kebab-case for file names\n- Prefer double quotes for strings over single quotes\n- Only add comments when it is necessary to explain code that isn't self-explanatory\n"},"items":[{"name":"CLAUDE.md","path":"CLAUDE.md","title":"CLAUDE.md","content":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Project Overview\n\nRallly is a meeting scheduling application built with Next.js that allows users to create polls to find the best meeting times. It supports both self-hosted and cloud-hosted deployments with a freemium model.\n\n**Core Technologies:**\n- Next.js 16 with React 19\n- tRPC for API layer\n- Prisma with PostgreSQL\n- Better-Auth for authentication\n- TailwindCSS for styling\n- TypeScript throughout\n- dayjs for date handling\n\n## Development Commands\n\n### Getting Started\n```bash\n# Install dependencies\npnpm install\n\n# Setup environment\ncp apps/web/.env.sample apps/web/.env\ncp packages/database/.env.sample packages/database/.env\n\n# Generate Prisma client\npnpm db:generate\n\n# Start development database\npnpm docker:up\n\n# Reset/setup database with seed data\npnpm db:reset\n\n# Start development server\npnpm dev\n```\n\n### Common Commands\n```bash\n# Development\npnpm dev                    # Start web app dev server\npnpm dev:landing           # Start landing page dev server  \npnpm dev:emails            # Start email template dev server\n\n# Building\npnpm build                 # Build web app\npnpm build:web            # Build web app with version injection\npnpm build:landing        # Build landing page\npnpm build:test           # Build for testing\n\n# Database\npnpm db:migrate           # Run database migrations\npnpm db:push              # Push schema changes\npnpm db:deploy            # Deploy migrations (production)\n\n# Testing\npnpm test:unit            # Run unit tests (Vitest)\npnpm test:integration     # Run integration tests (Playwright)\n\n# Code Quality\npnpm check                # Run Biome linter/formatter\npnpm check:fix            # Auto-fix linting issues\npnpm type-check           # Run TypeScript type checking\n\n# Utilities\npnpm i18n:scan            # Scan for translation keys\npnpm sherif               # Check package dependencies\n```\n\n## Architecture\n\n### Monorepo Structure\n- `apps/web/` - Main Next.js application\n- `apps/landing/` - Marketing/landing page\n- `apps/docs/` - Documentation site\n- `packages/` - Shared packages:\n  - `database/` - Prisma schema and client\n  - `ui/` - Shared UI components\n  - `emails/` - Email templates\n  - `posthog/` - Analytics client\n  - `billing/` - Stripe integration\n  - `utils/` - Shared utilities\n\n### Key Features & Structure\n- **Polls**: Core scheduling functionality in `apps/web/src/features/poll/`\n- **Spaces**: Workspace/team organization in `apps/web/src/features/space/`\n- **Authentication**: Better-Auth config in `apps/web/src/lib/auth.ts`, domain logic in `apps/web/src/features/auth/`\n- **tRPC API**: Routers in `apps/web/src/trpc/routers/`\n- **Feature Flags**: Quick create and other toggles in `apps/web/src/lib/feature-flags/`\n\n### Database\n- PostgreSQL with Prisma ORM\n- Multi-model schema split across files in `packages/database/prisma/models/`\n- Supports both cloud (Vercel KV) and self-hosted Redis for rate limiting\n\n### Authentication & Authorization\n- Better-Auth with multiple providers (Google, Microsoft, OIDC, email OTP, guest)\n- CASL-based permissions system for spaces and polls\n- User roles: admin, member with tier-based abilities (free/pro)\n\n### Deployment Modes\n- **Cloud Hosted**: Full SaaS with Stripe billing on Vercel\n- **Self Hosted**: Docker-based deployment without billing features\n- Environment variable `NEXT_PUBLIC_SELF_HOSTED=true` toggles features\n\n## Testing\n\n### Integration Tests (Playwright)\n- Located in `apps/web/tests/`\n- Use `pnpm test:integration` to run\n- Test utilities in `apps/web/tests/test-utils.ts`\n- Mailpit integration for email testing\n\n### Unit Tests (Vitest)\n- Use `pnpm test:unit` to run\n- Test files co-located with source code\n\n## Git Commits\nAlways use gitmoji prefixes in commit messages. Follow the gitmoji convention (https://gitmoji.dev) for the type of change (e.g. `📝` docs, `♻️` refactor, `🐛` fix, `✨` feature).\n\n## Code Standards\n\n### Styling\n- TailwindCSS with custom design system\n- Use `cn()` from `@rallly/ui` to compose classes\n- Biome for code formatting (indent: 2 spaces, double quotes)\n- Custom UI components in `packages/ui/src/` (shadcn-ui components go here)\n\n### UI Copy\n- **Sentence case** for all UI copy: capitalize the first word and proper nouns only. Applies to buttons, dialog titles, page and section headings, setting row titles, empty states, form labels, menu items and toast titles.\n- Exceptions — the test is **\"does the string name a thing?\"**, not \"is it a heading?\":\n  - Proper nouns and brands: \"Google Calendar\", \"Microsoft Calendar\"\n  - Initialisms stay uppercase, the rest lowercases: \"API keys\", \"Download ICS file\"\n  - Product feature names: \"Event Types\", \"Quick Create\", \"Control Panel\"\n  - Sample/placeholder data standing in for user content: \"Jessie Smith\", \"My Team\"\n- **Headings and dialog titles are not exempt.** Title Case has no single agreed rule (Chicago, AP and APA disagree), so it cannot be applied consistently; it degrades screen reader pronunciation and removes word-shape cues used by readers with dyslexia and low vision; and the same string often serves as both a button label and a dialog title, so a position-based rule would force two strings for one concept.\n- **Changing `defaults` alone is not enough.** `pnpm i18n:scan` will not overwrite an existing key's value — the UI keeps rendering the old copy. Run `pnpm i18n:sync` (`--sync-primary`) to push changed English values through. Never run `--sync-all`; it clears other locales' translations.\n\n### State Management\n- tRPC with TanStack Query for server state\n- React Context for client state (auth, preferences, etc.)\n- Form state with react-hook-form + Zod validation\n\n### TypeScript Conventions\n- Prefer inline prop types over named interfaces for simple component props\n- Example: `function Component({ prop }: { prop: string })` instead of defining a separate interface\n- Only create named interfaces when they're reused or complex\n- Create separate import statements for types (use `import type`)\n- Prefer `React.useState`, `React.useEffect`, etc. over standalone imports (`useState`, `useEffect`)\n- Prefer implicit over explicit return types\n\n### Dialog Management\n- **IMPORTANT**: Always use the `useDialog` hook from `@rallly/ui/dialog` for managing dialog state instead of manual `useState` for open/close state\n- The hook provides `dialog.trigger()`, `dialog.dismiss()`, and `dialog.dialogProps` which should be spread onto the dialog component\n- Example usage:\n  ```tsx\n  const dialog = useDialog();\n  \n  // Trigger dialog\n  <Button onClick={() => dialog.trigger()}>Open</Button>\n  \n  // Dialog component\n  <MyDialog {...dialog.dialogProps} />\n  ```\n\n### Component Conventions\n- Prefer composable components in the style of shadcn UI over large monolithic components\n- Keep component props minimal — pass only the bare minimum information needed\n- Add `\"use client\"` directive to the top of any `.tsx` file that requires client-side JavaScript\n\n### Streaming & Server Data Access\nThese rules prepare the app for Next.js `cacheComponents` (static shell + streamed dynamic content). Follow them in all new and touched server code:\n- Never await runtime data (session, `cookies()`, `headers()`, tRPC prefetches) at the top of a layout or page. Extract the awaits into a private async component rendered under a `<Suspense>` boundary (see the gate pattern in `app/[locale]/(space)/layout.tsx`) so the shell streams immediately.\n- Pass `params`/`searchParams` promises down into Suspense-wrapped children and await them there, instead of awaiting at the top of the page.\n- Don't call argless `dayjs()`, `new Date()`, `Date.now()`, or `Math.random()` during server render outside request-bound components — synchronous IO fails prerendering once `cacheComponents` is enabled. Compute \"now\" on the client or behind a Suspense boundary after `connection()`.\n\n### Clock-Classified Reads (upcoming/past/relative-to-now)\nReads filtered or grouped by the viewer's present (\"upcoming\", \"past\", agenda groupings) have no cacheable answer: the classification depends on the viewer's clock and timezone. All-day events are floating calendar dates (RFC 5545), so classifying them requires the viewer's current calendar date — never drop the timezone from such a query; that silently substitutes UTC as the viewer's zone. Choose the transport by what the data is to the surface:\n- **Content of a long-lived surface** (events list, agenda/calendar): fetch on the client — server renders the shell, the client queries with `getBrowserTimeZone()` and revalidates (refetch on focus). This gives the fastest paint and keeps the data fresh while the page stays open.\n- **Passing annotation on navigation chrome** (a count badge on a tile): a server snapshot is correct. Resolve the zone device-cookie-first via `getDeviceTimeZone()` from `@/lib/datetime/server` (falls back to the stored `user.timeZone`, then UTC). The session zone override is a poll-viewing aid and must not affect classification; `getDeviceDateTimeConfig` (which honors it) is for display on public pages.\n- **Never server-prefetch a client query with a zone the client will disagree with** (e.g. prefetching with the server's own zone and refetching after hydration). Seed `initialData` from the device cookie zone or render a skeleton until the client fetch lands.\n\n### File Organization\n- Route handlers follow Next.js App Router conventions\n- Always use kebab-case for file names\n\n### Directory Structure (apps/web/src)\n\n**Layering** — import direction is `app → features → components → lib`. Each layer may import from layers to its right, and anything may import `lib`. Nothing outside `app/` imports `@/app/*`. Boundaries are lint-enforced via `noRestrictedImports` in `apps/web/biome.json`; existing violations live on a shrinking migration allowlist there.\n\n- `app/` — routing only. Pages/routes are thin adapters composing `features/*`. A route-private `components/` folder is allowed for exactly one route segment; the moment a second segment needs a component, it moves to the owning feature.\n- `features/<domain>/` — the product (see below).\n- `components/` — shared, domain-agnostic UI only. Admission test: \"could this ship in a different product?\" Must not import from `features/` or `app/`. Cross-app reusables graduate to `packages/ui`.\n- `lib/` — infrastructure and cross-cutting clients (auth config, cache, datetime, errors, feature flags, rate limiting, storage). Never imports from `features/`, `components/`, or `app/`.\n\n**What is a feature** — a feature owns at least one of: a database entity, an external integration, or server-side lifecycle logic. UI-only folders are NOT features — they belong in `components/` or an owning feature's `components/`. Create the folder when the first server logic for a new domain noun appears — never speculatively, never for a component alone. Sub-concerns are subdirectories of their parent feature (e.g. `space/member/`), not sibling features.\n\n**Feature file vocabulary** — closed set; new file names require a team decision. Applies recursively in sub-concern directories:\n\n- `data.ts` — parameterized reads (Prisma queries), must start with `import \"server-only\"`. Trusts its input: every query takes explicit arguments and carries its tenant scope in the where clause. Never reads the request — no `next/headers`, `next/navigation`, or session state (lint enforced)\n- `loaders.ts` — request facing reads: resolve the actor from the session, apply page semantics (redirects to /login and /setup, `InvalidSessionError` on bans), and delegate to `data.ts` with proven scope. Loaders always consume `data.ts`, never the reverse, and never import `@rallly/database` — so query logic cannot fork between the session path and the API path. Server components call loaders; API routes, webhooks and cron call `data.ts` with proven scope from their own gate (API routes are lint banned from `@/features/**/loaders`). Naming note: `session-data.ts` and `queries.ts` were considered and rejected\n- `mutations.ts` — writes + cache invalidation, must start with `import \"server-only\"`. Trusts its input: takes explicit parameters (`userId`, not headers), no session reads, no `headers()`, callable from system contexts (webhooks, cron, moderation). Authorization happens in `actions.ts` (safe-action middleware + CASL against the database). Never call Better-Auth endpoints from a mutation — they resolve the target user from request headers and authorize against the session snapshot; use adapter-level APIs (`authLib.$context` → `internalAdapter`) instead\n- `actions.ts` — `\"use server\"` actions, thin wrappers over mutations, validated via safe-action + `schema.ts`. Owns authentication and authorization (safe-action clients + CASL checks against database state, never against the session snapshot). Exception to the thin-wrapper rule: writes whose target user is defined by the session (self-profile updates) call the Better-Auth endpoint directly in the action — the endpoint refreshes the session snapshot and cookie cache in one step. Rule of thumb: target user from a parameter → mutation via `internalAdapter`; target user from the session → Better-Auth endpoint in the action\n- `schema.ts` — Zod schemas (isomorphic, no Prisma imports)\n- `types.ts` — domain types\n- `ability.ts` — CASL permissions\n- `constants.ts`\n- `utils.ts` — pure domain helpers, co-located `*.test.ts`\n- `client.tsx` — client entry: providers, context, hooks, stores (no separate `hooks.ts` or store files)\n- `service.ts` — external integration client (class/factory wrapping a third-party API), must start with `import \"server-only\"`\n- `components/` — feature UI (no components at feature root)\n- `assets/` — static files used by the feature\n- NOT allowed: `index.ts` barrels, `helpers.ts`, `queries.ts`, `hooks.ts`, `lib/`, `libs/`\n\n**Read/write symmetry** — the request facing / core split is the same on both sides:\n\n|  | Request facing (session, redirects, auth) | Core (parameterized, trusts input) |\n| -- | -- | -- |\n| Reads | `loaders.ts` | `data.ts` |\n| Writes | `actions.ts` | `mutations.ts` |\n\n**Loader placement** — pages, layouts and route-private components never call `data.ts` directly: every read they consume goes through a loader in `features/<domain>/loaders.ts`, so the session gate that proves scope cannot be forgotten at the call site (lint-enforced: `@/features/**/data` is banned in `app/**`; existing violations sit on a shrinking migration allowlist in `apps/web/biome.json`). A loader bundles the gate and the read: `import \"server-only\"`, named with a `load*` prefix, wrapped in React `cache()` so every consumer in a request shares one read, resolves the actor via the session gates (`requireUser()`, `getActiveSpace()` — both trust the session cookie cache; `getCurrentUser()` is the database-verified read for pages that need DB-fresh user state) and delegates to `data.ts` with proven scope. Reference shape: `features/notifications/loaders.ts` (`loadNotificationPreferences`). Route-private `actions.ts` files are the write side, not pages — they keep calling `data.ts`/`mutations.ts` with proven scope from their safe-action gate (loaders' page semantics, redirects, don't belong in actions) and are exempt from the lint ban.\n\n**DAL enforcement** — `@rallly/database` may only be imported from `features/**/data.ts` and `features/**/mutations.ts` (lint-enforced via `noRestrictedImports`; existing violations sit on a shrinking migration allowlist in `apps/web/biome.json`; `loaders.ts` is absent from the allowlist, so database imports are banned there by default). Parameterized reads take their tenant scope as `spaceId: AuthorizedSpaceId` (from `@/features/space/types`); only the session gate (`createSpaceDTO`) and the API key middleware may cast to it. API routes under `app/api/**` (except tRPC and better-auth) must not import `@/features/**/loaders` — they authenticate their own way and pass proven scope to parameterized reads. The inverse holds for pages: `app/**` (outside `app/api/**` and route-private `actions.ts`) must not import `@/features/**/data` — reads reach pages only through loaders. `api/private` handlers own serialization and must parse response bodies through their zod schemas.\n\n**Cross-feature imports** — allowed, public surface only (the vocabulary files above); never reach into another feature's internals. No cycles between features (CI-enforced). For UI, prefer composing multiple features at the page level in `app/` over feature-to-feature component imports.\n\n**`trpc/` is frozen legacy transport** — queries only; routers call `features/*/data.ts`. No new mutations — writes are server actions calling `features/*/mutations.ts`.\n\n### PostHog Event Naming\n- Use the `category:object_action` pattern\n- Lowercase only, snake_case, present-tense verbs\n  - **category** — the context/flow (e.g. `poll_creation`, `account_settings`)\n  - **object** — the component/element (e.g. `invite_link`, `manage_button`)\n  - **action** — what happened (e.g. `click`, `copy`, `submit`)\n- Example: `posthog?.capture(\"poll_creation:manage_button_click\")`\n\n## i18n & Localization\n\n- i18next for internationalization\n- Translation files in `public/locales/[lang]/`\n- Crowdin integration for translation management\n- Use `pnpm i18n:scan` to extract new translation keys\n- **IMPORTANT**: When TypeScript errors occur for missing i18n keys, run `pnpm i18n:scan` instead of manually adding keys. This command automatically scans the codebase for `Trans` components and generates the necessary translation entries.\n- **IMPORTANT**: Never manually add translations to `.json` files. This is handled by tooling.\n- **Pluralization**: Always use ICU message format for plurals. Example: `{count, plural, =0 {No items} one {1 item} other {# items}}` instead of separate singular/plural translation keys.\n- i18n keys are in camelCase and should describe the message (e.g. `\"lastUpdated\": \"Last updated\"`)\n- If an i18n key is not intended to be reused, prefix it with the component name in camelCase\n- In client components, use the `<Trans>` component from `@/i18n/client` with the `defaults` prop:\n  ```tsx\n  import { Trans } from \"@/i18n/client\";\n  <Trans i18nKey=\"menu\" defaults=\"Menu\" />\n  ```\n- When a server component needs `t` or `i18n` itself — for a string outside JSX, or to render `TransWithoutContext` — get them from `getTranslation` in `@/i18n/server` (rendering the client `Trans` instead needs neither; see below):\n  ```tsx\n  import { Trans } from \"react-i18next/TransWithoutContext\";\n  import { getTranslation } from \"@/i18n/server\";\n\n  const { t, i18n } = await getTranslation();\n  t(\"menu\", { defaultValue: \"Menu\" });\n  <Trans t={t} i18n={i18n} ns=\"app\" i18nKey=\"menu\" defaults=\"Menu\" />;\n  ```\n  Whenever you use `TransWithoutContext`, pass **both** `t` and `i18n` — omitting `i18n` falls back to the module-global instance, which can carry another request's language under concurrency.\n- **Per-request instances are what make this safe.** `apps/web` (`i18n/client.tsx`, `initI18next`) and `packages/emails` (`createEmailI18n`) both build a fresh instance per render via `createInstance()`, so nothing is shared between concurrent requests. Any new i18n entry point must do the same — never `import i18next from \"i18next\"` (the package default export is a process-wide singleton) and never mutate a shared instance with `changeLanguage()` to serve a request. `apps/landing/src/i18n/` currently violates this and is the one place where passing `i18n` does *not* help, because the `i18n` it hands you **is** the singleton.\n- **Where each `Trans` is required**, and where it's a free choice:\n  - **Emails** — `TransWithoutContext` only, with `t` and `i18n` from `createEmailI18n(locale)` (`packages/emails/src/i18n.ts`). Emails are sent from server components, where React context is unavailable, and each render builds its own locale-bound instance so a concurrent fan-out can't cross languages. Only a Next build catches a violation.\n  - **`app/global-error.tsx`** — neither. It renders outside the `[locale]` segment that mounts `I18nProvider`, so there is no locale and no `t`. Copy there is hardcoded English.\n  - **Everywhere else** — either import is correct. `I18nProvider` is mounted in `app/[locale]/layout.tsx`, the root layout, so every page (public poll pages included) is inside it. A server component *may* render the `\"use client\"` `Trans` from `@/i18n/client`: React serializes it as a client reference and the browser renders it against the per-tab instance, so there is no cross-request language bleed and no extra bundle weight (`react-i18next` is already loaded app-wide). Prefer `TransWithoutContext` in server components to keep static copy out of hydration, but don't make an otherwise-static page `async` just to do it — and don't file the client import as a bug.","category":"root","tokens":5231},{"name":".cursorrules","path":".cursorrules","title":".cursorrules","content":"- Use pnpm for package management\n- Use dayjs for date handling\n- Use tailwindcss for styling\n- Use react-query for data fetching\n- Use react-hook-form for form handling\n- Prefer implicit return values over explicit return values\n- Use zod for form validation\n- Create separate import statements for types\n- All text in the UI should be translated using either the Trans component or the useTranslation hook\n- Prefer composable components in the style of shadcn UI over large monolithic components\n- DropdownMenuItem is a flex container with a preset gap so there is no need to add margins to the children\n- Keep the props of a component as minimal as possible. Pass only the bare minimum amount of information needed to it.\n- All text in the UI should be translatable.\n- i18n keys are in camelCase.\n- Use the <Trans> component in client components from @/i18n/client. Use the `defaults` prop to provide the default text. Example:\n\n```tsx\nimport { Trans } from \"@/i18n/client\";\n\n<Trans i18nKey=\"menu\" defaults=\"Menu\" />\n```\n\n- On the server use the `getTranslations` function from @/i18n/server to get the translations. Example:\n\n```ts\nconst { t } = await getTranslations();\n\nt(\"menu\", { defaultValue: \"Menu\" });\n```\n\n- shadcn-ui components should be added to packages/ui\n- Always use a composable patterns when building components\n- Use `cn()` from @rallly/ui to compose classes\n- Prefer using the React module APIs (e.g. React.useState) instead of standalone hooks (e.g. useState)\n- Do not attempt to fix typescript issues related to missing translations. This will be handled by our tooling.\n- Never manually add translations to .json files. This will be handled by our tooling.\n- Add the \"use client\" directive to the top of any .tsx file that requires client-side javascript\n- i18nKeys should describe the message in camelCase. Ex. \"lastUpdated\": \"Last Updated\"\n- If the i18nKey is not intended to be reused, prefix it with the component name in camelCase\n- Always use kebab-case for file names\n- Prefer double quotes for strings over single quotes\n- Only add comments when it is necessary to explain code that isn't self-explanatory\n","category":"root","tokens":535}]}