Gotchas and house conventions the code does not make obvious.
pnpm monorepo for the daily.dev app suite:
packages/webapp: Next.js Pages Router, not App Router (app/exists only for the service worker and the not-found page), deployed on Vercel.packages/extension: Chrome/Edge/Opera extension.packages/shared: components, hooks, GraphQL, design system. Code used by both surfaces lives here.packages/storybook,packages/playwright, eslint/prettier config packages.- Storybook is designer territory: Vercel deploys it only when
packages/storybook,packages/sharedor the rootvercel.jsonchanges (ignoreCommand), and it has no CI job, no strict typecheck, and.gitattributescollapses its diffs on GitHub. Don't add checks for it.
- Jest run outside the package scripts needs
NODE_ENV=test, otherwise React/RTL run as a production build. - Don't run
buildwhile a dev server runs; it breaks hot reload. - Run
node ./scripts/typecheck-strict-changed.json changed.ts/.tsxfiles; package-wide strict tsc has unrelated backlog. - CI has no webapp
next build, so after shared changes run the full webapp tsc before pushing or Vercel catches what CI missed. - After changing a shared component, run
pnpm --filter webapp testtoo. Cross-package tests assert DOM structure and break silently otherwise.
- Write code that reads like the surrounding code: match its comment density, naming, and idioms. The reasoning behind a fix goes in the commit message, not a comment above the code.
- Tests exist to catch real regressions, not to raise coverage. Skip tests that duplicate an existing one or assert implementation details that break on harmless refactors.
- Import from source files, never through barrel
index.tsfiles, and don't add new barrels. - Queries are options-creator functions spread into
useQuery(seepackages/shared/src/hooks/AGENTS.md). Page or feature scoped state usescreateContextProviderfrom@kickass-coderz/react(seecontexts/ActivePostContext.tsx). - GraphQL has no codegen. Types in
packages/shared/src/graphql/types.tsand the domain files are hand-written; update them with the query you edit. - Every GraphQL query is its own round trip unless it opts into
gqlBatchRequest(graphql/batch.ts), which coalesces the queries fired in the same tick into one POST behind thegql_batchingflag. Paint-blocking queries (boot, feeds, the post query) stay ongqlClient.request, and mutations never batch. - Data the shell needs on every page load for a logged-in user (actions, streak, feed list) comes from one
SHELL_STATE_QUERY(graphql/shellState.ts) thatShellStateProviderfans out into the per-hook cache keys, so add a root field there instead of a new standalone shell query and gate the hook onuseShellState().isSettledso it only fetches on its own as the fallback. Never merge boot, the feed, or the post query with anything slower, and gate widget queries on the surface that renders them. - Payment providers are per platform (
contexts/payment/index.tsx): Paddle on web, StoreKit on iOS, a Chrome extension variant. Don't assume Paddle. - Native iOS/Android wrappers run the webapp shell, so
isExtension/BootAppcan't tell them apart. Platform comes fromgetDailyClientPlatform(version)inpackages/shared/src/lib/func.ts, never from an extension/webapp boolean.
- Flags live in
packages/shared/src/lib/featureManagement.ts. Never default an experiment flag totrue: the default is the control value, and a truthy default ships the experiment to everyone on merge with no rollback short of a deploy. useConditionalFeaturewithshouldEvaluateso a flag is only evaluated when the component would render.- When removing a flag, match the product request: delete the gated behavior or keep it permanently, and remove the discarded path's code and tests.
- Semantic tokens (
text-primary,bg-surface-float,border-border-subtlest-tertiary) andtypo-*classes only; theno-custom-colorESLint rule blocks raw palette and hex colors, which also break theme switching. - No raw
pxin arbitrary Tailwind values: the scale for multiples of 4,remotherwise (h-[1.125rem]). - One-off styles outside the token system go in
packages/shared/src/styles/custom.ts. - Full-screen media lightboxes need a dark overlay (
bg-overlay-primary-pepper), not the Modal defaultbg-overlay-quaternary-onion. Close buttons over images useButtonVariant.Primary;Floatvanishes over photos. - Dismissible banners/cards use the shared
CloseButton, not a full-width text "Dismiss" button. - CSS Grid feed: an item's intrinsic content height stretches the whole row even under
max-h-*. Wrap content in anabsolute inset-0 flex flex-colchild and give the Cardmin-h-card(seeArticleGrid.tsx). Such constraints go on the grid variant of shared cards only.
- User-facing copy is American English, never British:
color,behavior,favorite,honor,center,catalog,gray,canceled,-ize/-ization(customize,organization,optimize). This covers JSX text, labels, placeholders,aria-labels, toasts, error messages, emails, public docs, and CLI output. Fixed external names stay as they are (aria-labelledby, API enum values likeCANCELLED). - Shared sections render on several surfaces (header popover and standalone page, regular and Plus); apply changes to every instance.
- Activity list modals (reposts, upvotes, history) reuse feed card primitives (
FeedItemContainer,PostCardHeader): compact rows, no dominating images. - User-facing limits get helper copy, not
3/5counters, unless product asks for progress UI. - Client-side mirrors of backend validation limits should use named constants shared by the schema, UI, and tests; don't repeat numeric limits inline.
- Render what the query returns; backend access control decides visibility, not client heuristics like
source.public. - On search pages
MainFeedLayoutrenders pagechildrenafter the<Feed>; content above results goes throughsearchChildreninlayoutProps. - Tag labels render the backend
flags.titleor the raw value (#react), never client-derived casing; bare tag strings get titles fromtagTitlesQueryOptions. A keyword's own page title (<title>, H1, JSON-LD) keeps theformatKeywordfallback for SEO.
- Infinite scroll: pass
fetchNextPage,canFetchMore(fromhasNextPage), andisFetchingNextPageas separate props; never derivecanFetchMorefrom callback existence (seeInfiniteScrolling.tsx). - Portaled drawers/overlays must
stopPropagationon the overlay click, otherwiseuseOutsideClickcloses the parent modal (seedrawers/Drawer.tsx). - Next's scroll restoration is off, so the router scrolls to the top on every route change, back/forward included.
useScrollRestoration, mounted once in_app, saves each history entry's position as you leave it and restores it on back/forward for every page; it must wait for the page to reach full height, or phones get stranded mid-feed. Never turn back into a full page load (beforePopStatepluslocation.href): the reload drops the saved positions and the query cache, so readers land at the top. - Markdown conversion: never run formatting regexes over already-generated HTML (image
srcURLs contain_).
Update .nvmrc, Dockerfile, .github/workflows/e2e-tests.yml, .circleci/config.yml (multiple spots), packages/playwright/package.json engines, the volta field in the root package.json, and README.md. Then pnpm install and commit the lockfile.
packages/{webapp,extension,playwright}/AGENTS.md, packages/shared/src/{components,hooks}/AGENTS.md.