Skip to content

Instantly share code, notes, and snippets.

@nfarina
Created April 27, 2026 22:45
Show Gist options
  • Select an option

  • Save nfarina/e8adcc7a3a91bc571118db1474a97e68 to your computer and use it in GitHub Desktop.

Select an option

Save nfarina/e8adcc7a3a91bc571118db1474a97e68 to your computer and use it in GitHub Desktop.
Claude Handbook
paths
packages/admin/**
packages/app/**

App Package

Our client-side UI is written exclusively using an in-house, bespoke UI framework called "Crosswing".

Structure

We develop locally with Vite, and attempt to lazy() load top-level sections of the app to improve the development UX.

Data Model

We enforce using firewing data access layers (see .claude/rules/firewing.md) to read/write most things to the DB. For instance, when working with User records, we first write const Users = useUsers() which loads the data access layer packages/app/users/useUsers.tsx and provides all the read/write/query methods we need. Note the capitalization! This may seem strange for the local var to be const Users instead of const users, but it matches what we do on the server side, where we have a class Users for our server code's data access layer. Also, we often want to reserve a users var for an array of User objects that we load from the DB.

Our core record types are:

  • User: Represents a user that can login to our system.
  • Team: All users must be a member of some team to use the app.

Context

We use React Context extensively to avoid "prop drilling" and creating excessive parameters on components. Some very common context hooks you'll need are:

const { userId } = use(CurrentUserContext);
const { teamId } = use(CurrentTeamContext);

Paths

We use Crosswing's packages/crosswing/router/Router.tsx for navigation, and we prefer using packages/crosswing/router/Link.tsx components or "subclasses" (styled(Link)) for clickable navigation elements.

Additionally, we try to centralize our various paths in packages/db/paths.

Welcome! We have documented the overview of this project in /README.md next to this file.

You can also find a growing collection of documentation in /.claude/rules. They are named after the top-level folders in /packages that they relate to.

Environment setup

Check the CLAUDE_CODE_REMOTE and CODEX_REMOTE environment vars - if either are true, then you'll want to read all of CLOUD.md. Otherwise, assume you're running on a developer's local machine and any needed services are already running.

Most important things

  • Use yarn tsc to check types after any changes. No other build or linting tools are needed.
  • If you are working on tests, you can run them with yarn vitest:unit which accepts all vitest arguments like the name of the test file.
  • Don't start up services/servers/Storybook/etc., I've already got them all running.
  • Don't use useMemo/useCallback - we are running our code through React Compiler which auto-memoizes these things in a far more efficient way.
  • We prefer function parameter types to be inline with the function definition, so prefer function myFunc({ someParam }: { someParam: SomeType }) over function myFunc({ someParam }: MyFuncParams)
  • For async work in UI, we almost exclusively use useAsyncTask which handles the lifecycle of tasks from simple to complex. Look for examples in the codebase. Don't use try/catch (React Compiler can't handle it) and instead use onError/onFinally since useAsyncTask.func catches any errors.
  • We don't typically create tiny "convenience" hooks like useSomeContext() or useSomeDialog() and instead prefer use(SomeContext) and useDialog(() => <SomeDialog>).

Coding guidelines

Here we have a collection of "oh and" sort of notes that will be added to over time.

  • All comments (going forward) are in Sentence case and end with a period (or other appropriate punctuation).
  • Our UI (again, going forward) is Sentence case as well. For a dialog, use "Enter your name" and NOT "Enter Your Name". For a button, use "New template" and NOT "New Template". Etc.
  • One pattern we use for "work in progress" components is export function SomeComponent({}: any) { which allows us to pass arbitrary params as we design our component. This goes away once we add our first "real" param.
  • TypeScript types packages (@types/xyz) are always installed as dev dependencies in the root package.json instead of individual workspace projects.
  • If you add/change cli scripts, tasks, or rpc files, run yarn workspace cloud gen to update endpoints.ts.

Cloud Environment Setup

Non-obvious setup steps for getting the cloud runtime environment ready.

Initial Setup

To execute any of our scripts or servers, or to run tests, you'll first need to execute:

corepack enable
yarn install
yarn run build:tools
cp packages/cloud/google.mock.json packages/cloud/google.json

Now you can yarn run tsc to check types.

Tests

If you are working on server-side code, you can check your work by running tests:

yarn run vitest someTestFile.test.ts

Storybook

One great way to "see" UI immediately without starting a ton of services is Storybook. To make Storybook work:

Fix NO_PROXY for Network Access

This section is only relevant if CLAUDE_CODE_REMOTE was true

The Claude Code cloud environment includes *.googleapis.com in NO_PROXY, but the container has no local DNS - only proxy-based resolution. This causes Playwright browser downloads (and other googleapis requests) to fail with EAI_AGAIN DNS errors.

Fix by removing googleapis from NO_PROXY:

export NO_PROXY=$(echo "$NO_PROXY" | sed 's/,\*\.googleapis\.com//g; s/,\*\.google\.com//g')
export no_proxy="$NO_PROXY"

Then yarn playwright install chromium will work.

Run Storybook

yarn storybook --ci &

Emulators

To run the webapp, or server scripts pointed at the emulator, you'll need to spin up the emulator server:

# Install Firebase CLI
npm i -g firebase-tools

# Setup Emulated Firestore initial data
yarn workspace cloud script:emulated fixtures

# Start services (in background):
yarn workspace cloud run watch       # Bundles server code with Rolldown
yarn workspace cloud serve:emulated  # Firebase emulators (needs NO_PROXY fix)

IMPORTANT: The Firebase Functions Emulator currently does not work. So the "Scripts" section below is still valid, but the "App/Admin Servers" section won't actually work yet.

Scripts

You can write code in packages/cloud/util/scripts/scratchpad.ts and execute it against the local emulator. First you'll need to spin up the local emulation services above. Then you can execute the scratchpad (or other scripts) with:

yarn workspace cloud script:emulated scratchpad  # Runs scratchpad.ts
yarn workspace cloud script:emulated             # Lists available scripts

You can make new scripts, but if you do, you'll need to run yarn workspace cloud gen first.

App/Admin Servers

You can run the App and/or Admin servers:

# Start services (in background):
yarn workspace app run dev              # App site
yarn workspace admin run dev            # Admin site

Then browse to the app or admin site with Playwright (must be installed as described above for Storybook). You'll need to log in first, and you must remember to enable "Local Firestore" at the sign-in screen. This will reload the page and from then on the app/admin will be talking to local emulators ("Local Functions" will be enabled by default after enabling local firestore).

For the app, you may sign in as nick@example.com. For admin, use admin@example.com. These are both from fixtures.

paths
packages/db/**
packages/cloud/**
packages/app/**
packages/admin/**

DB Package

Our entire database schema is located in packages/db.

All known Firestore collections can be found in packages/db/data.ts. This fuels the type-safety found in modules like packages/app/util/useProjectCollection.tsx and packages/cloud/util/getProjectCollection.ts.

Schema Design

Always think VERY HARD before modifying our schema! Consider different approaches and ask for feedback before making significant changes. Our schema is the very core of our system and changes should not be made lightly. Unlike a relational database, Firestore schemas must be designed with queries considered in advance! Firestore is wonderful in that it makes your app "always up to date" without ever needing to refresh the page, but the tradeoff is inflexible querying (no joins). For example, if you want to be able to efficiently display a paged list of Orders, the Order DB type should contain all the data needed to render the list the way you want without performing an additional fetch per-order. So if you want the list of Orders to show the Customer name, it's not enough to just store Order.customerId; the actual name (and profile pic if you want it) needs to be embedded inside the Order object. Typically we do this sort of thing with "stubs", so you'd have Order.customer which would be a CustomerStub and would contain a subset of properties like Customer.name, Customer.avatar, etc. using a Pick<> TypeScript type.

We endeavor to make all DB objects "plain old JavaScript objects" without any of Firestore's "fancy" classes like Timestamp. DB objects should be JSON serializable. Firestore's SDK will by default give you "Document data" separate from "Document ID", but we like to combine these into one object with an id property like this:

export type Template = {
  id: string;
  variations?: TemplateVariations | null;
}

This way we can pass around single objects (or arrays of objects) without needing to pass IDs separately.

For "array" type properties, we greatly prefer using a Record<> type instead of relying on Firestore's internal array ordering. This forces us to have some durable ID property which will serve as the Record key, and helps to facilitate concurrent data modification. Here's a simple example:

import { flattenObjectProperty, OmitId } from "./flatten";

export type TemplateVariations = Record<string, OmitId<TemplateVariation>>;

export type TemplateVariation = {
  id: string;
  created: number;
  /** Sort order, if necessary. */
  sort: number;
  /** Name of the variation; auto-generated by the AI. */
  name: string;
  

We prefer having top-level exported "simple functions" to perform operations on DB objects, like converting a Record type to an Array while including the Record key as an "id" property, or formatting the name of a User object, creating a "stub" from a full object, etc. An example building on the above:

export function getTemplateVariations(
  variations: TemplateVariations | null | undefined,
): TemplateVariation[] {
  return sort(flattenObjectProperty(variations), "sort");
}

Here we use flattenObjectProperty which is a helper function that converts a Record into an Array of the same type but with id added. This way we can pass around TemplateVariation objects without a separate id property, just like our "top level" DB objects.

Any small "helper" function that is designed to work with DB objects and is ALSO isomorphic (preferred) should be placed in the same module as the DB object type. So getUserName() should be in the same users.ts file in packages/db near the export type User = { definition.

Firestore Indexes

Firestore supports "composite indexes" where you can have multiple where clauses in queries that include an inequality filter, like:

app().firestore().collection("users").where("deleted", "==", null).where("admin", "==", true).orderBy("created", "desc")

Because of our orderBy clause, for this query to work, we would need to create a composite index on [deleted, admin, created].

We try to limit our use of composite indexes, because there is a hard limit across the entire project on composite indexes, enforced by Google Cloud Firestore.

Paths

We try to centralize our paths in isometric classes with static methods:

  • packages/db/paths/AppPaths.ts: Contains known paths for packages/app, the user-facing SaaS app.
  • packages/db/paths/AdminPaths.ts: Contains known paths for packages/admin, our internal-facing admin site.
  • packages/db/paths/TeamPaths.ts: Contains known paths that are identical between app and admin; we prefer using these because we embed many components from app inside the admin site. This way, those components can use paths from TeamPaths and they will work on both app and admin.
  • packages/db/paths/SitePaths.ts: Contains known paths for packages/site, our marketing site (currently unused).
  • packages/db/paths/HttpPaths.ts: Contains known paths for our public-facing API in packages/cloud exported by the http function.
  • packages/db/paths/StoragePaths.ts: Contains known paths for things we store inside Firebase Storage, mostly images.

When rendering a Link or subclass thereof, we STRONGLY prefer absolute paths constructed using one of these classes, instead of relative paths, for consistency. When adding querystring params to paths, we strongly prefer using our buildPaths helper function in packages/db/urls.ts which serializes params easily and automatically creates links that stay in the local emulator (this is only relevant when making links NOT for <Link/> and friends).

Guidelines for Contributions

These guidelines are added to in response to PRs from new contributors.

  • Code Consistency: Strive to write code that is consistent with the existing codebase in terms of patterns, structure, and style. Before implementing new functionality or components, look for existing examples to follow. This is especially important when working with UI components, data access, and asynchronous operations.
  • Exception Handling Built-in: Most of our system is designed to handle unexpected exceptions. For instance, useAsyncTask on the client, or server RPC/Task methods, all can throw errors and it will be handled by the framework. No need for you to write excess try/catch statements "just in case". If you're on the server, throw UserFacingError if a message is suitable for displaying to the user (most are).

Be curious! This project is large but unusually consistent. Don't reinvent the wheel! There are many existing wheels inside for you to examine and use.

Project Overview

This project, Denada, is a "Generative AI" SaaS product targeted at people in marketing departments who need to make lots of creative. We help them by generating "Libraries" which are collections of "Blocks" which contain big blobs of complex HTML, and user-friendly exposed Parameters ("title", "image", etc.). The user creates Templates which are a JSON implementation of a Library with the desired Blocks and Parameters, and the rendering to final HTML is done using the Eta templating library. Users create templates through an AI Chat UI, or manually in the Template Editor.

Overall Structure

Our project is split into packages/* using Yarn Workspaces:

  • packages/admin: Our internal-facing admin site, deployed to https://admin.heydenada.com (locally at http://localhost:2303). See .claude/rules/admin.md.
  • packages/app: Our customer-facing UI, deployed to https://app.heydenada.com (locally at http://localhost:2302). See .claude/rules/app.md.
  • packages/workers: A small amount of highly optimized server code for critical paths like image hosting and transforming, and Puppeteer, deployed to the serverless Cloudflare Workers platform at https://images.heydenada.com and https://templates.heydenada.com.
  • packages/cloud: Our server code, deployed to the serverless Firebase Functions platform at https://cloud.heydenada.com (locally at http://localhost:2354). See .claude/rules/cloud.md.
  • packages/mocks: Pre-constructed mock DB objects for use in Storybook and tests.
  • packages/db: Our DB schema and helper functions, isometric for use in both browser and NodeJS. See .claude/rules/db.md.
  • packages/shared: Icons, images, and a couple small isometric utilities.
  • packages/figma: Our Figma plugin.
  • packages/firewing: Our data access layer. See .claude/rules/firewing.md.
  • packages/crosswing: Our UI platform. See .claude/rules/crosswing.md.

In an attempt to avoid circular dependencies, we've established the following package hierarchy, from highest to lowest.

  • admin can import code from app.
  • app can import types only from cloud.
  • workers can import code from cloud.
  • cloud can import code from mocks.
  • mocks can import code from db.
  • db can import code from shared.
  • shared is simply icons and images and imports nothing.
  • figma is standalone plugin code and imports nothing.
  • firewing is our DB platform and can import crosswing.
  • crosswing is our UI platform and imports nothing.

Package Organization

We attempt to unify the top-level folders inside each package according to the main type of data it's concerned with. So for instance, user-related functions are in a users subfolder which matches the name of packages/db/users.ts in the DB package: packages/admin/users, packages/app/users, packages/cloud/users, etc.

Chatwing

Much of our "AI" implementation is reusable across different projects (not in this repository), but is difficult to encapsulate in a separate package. So we created "Chatwing" which is a virtual package. Files that are part of Chatwing are marked with @chatwing towards the top of the file and are kept in sync with other Chatwing-based projects.

Sometimes we have shared code in a file that is mixed with project-specific code. For these cases, we have created "fenced code blocks" that are marked with comments like @chatwing:something:start and @chatwing:something:end. Our synchronization system will keep that project-specific code intact within each separate project.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment