Skip to content

Instantly share code, notes, and snippets.

@brennanmceachran
Last active July 29, 2025 13:42
Show Gist options
  • Save brennanmceachran/df42dc72cbb1d6aea9e648040154cd08 to your computer and use it in GitHub Desktop.
Save brennanmceachran/df42dc72cbb1d6aea9e648040154cd08 to your computer and use it in GitHub Desktop.
filepath
AGENTS.md

AGENT Guidelines

  • Before making changes, read .instructions/general.instructions.md and any relevant instructions inside .instructions/apps or .instructions/packages for the files you plan to modify.
  • Keep TypeScript strictly typed. Do not use @ts-ignore or ESLint disable comments.
  • Always run pnpm typecheck to ensure your code is working. This also builds any needed deps. So do this anytime you cross a package boundary.
  • Use pnpm lint to check for linting errors and code style.
  • Use pnpm format -w to format all packages before committing.
  • The Github PR workflow will run pnpm typecheck, pnpm lint, and pnpm format to ensure code quality and will not allow merging if there are issues.
filepath applyTo
.instructions/general.instructions.md
**/*

IMPORTANT RULES

  • IF you're opening/reading files, read them in chunks of 1000 lines at a time. NEVER read less.
  • NEVER, EVER, EVER use @ts-ignore or eslint-disable comments. It breaks our linter and we cannot deploy the code. Instead report back IF you're stuck (we likely just have to rebook the TS/ESLint server)

Turbo Repo General Guidelines

Repository Structure

  • apps/ - Client applications

    • nextjs/ - Next.js 15 web application (app router)
    • expo/ - Expo SDK 53 / React Native mobile application
  • packages/ - Shared libraries

    • api/ - tRPC API with routers and services
    • auth/ - Authentication using Better Auth
    • db/ - Database schema and client using Drizzle
    • ui/ - Shared UI components for web and native
    • validators/ - Shared validation schemas
  • tooling/ - Development tooling

    • eslint/ - ESLint configurations
    • prettier/ - Prettier configurations
    • tailwind/ - Tailwind configurations
    • typescript/ - TypeScript configurations

Code Style

  • Use TypeScript for all code

  • Follow ESLint and Prettier configurations

  • Use absolute imports with appropriate path aliases:

    // In Next.js app
    
    // For packages
    import { Button } from "@kidbook/ui";
    
    import { CamelCaseName } from "~/components/kebab-case-name";

Architecture Patterns

  • Full-stack TypeScript with end-to-end type safety
  • tRPC for type-safe API calls between client and server
  • Repository pattern for data access in API package
  • Drizzle ORM for database operations
  • Better Auth for authentication across platforms
  • Shared UI components with platform-specific implementations

Cross-Package Integration

  • Import from other packages using the package name:

    import { api } from "@kidbook/api/client";
    import { auth } from "@kidbook/auth/client";
    import { Button } from "@kidbook/ui";
  • Maintain consistent type usage between packages:

    // In UI
    import type { RouterOutput } from "@kidbook/api";
    
    // In API
    export type RouterOutput = inferRouterOutputs<AppRouter>;
    
    type Story = RouterOutput["story"]["getById"];

Development Workflow

  • Use Turborepo for task orchestration. These tasks should be run from the root of the monorepo:

    pnpm dev         # Start development server for all apps
    pnpm typecheck   # Run type checking across the repo
    pnpm lint        # Run linting across the repo
  • Create new packages with consistent structure:

    pnpm turbo gen package --name my-package
  • Create new apps with appropriate setup:

    pnpm turbo gen app --name my-app

Environment Variables

  • Use per-package environment validation:

    // In package/env.ts
    export const env = createEnv({
      server: {
        DATABASE_URL: z.string().url(),
      },
      client: {
        NEXT_PUBLIC_BASE_URL: z.string().url(),
      },
      runtimeEnv: process.env,
    });
  • Store development variables in .env.local files (not committed)

  • Access environment variables through the package's env.ts:

    import { env } from "~/env";
    // or
    import { env } from "@kidbook/package-name/env";

Documentation

  • Keep README files updated
  • Document key concepts and APIs with inline comments
  • Use TypeScript JSDoc for public functions and interfaces

Performance

  • Optimize bundle sizes with appropriate code splitting
  • Monitor and optimize API response times
  • Use appropriate caching strategies for data fetching
filepath applyTo
.instructions/tooling.instructions.md
tooling/**/*

Tooling Guidelines

ESLint Configuration

  • Extend base configurations for different project types
  • Keep rules consistent across the monorepo
  • Configure rules based on project type (React, Next.js, etc.)
  • Avoid disabling important linting rules

Prettier Configuration

  • Maintain consistent formatting across the codebase
  • Use the shared Prettier configuration
  • Don't override formatting rules in individual packages

Tailwind Configuration

  • Keep design system tokens consistent between web and native
  • Extend the base configuration for specific platform needs
  • Follow naming conventions for custom theme extensions

TypeScript Configuration

  • Extend the base tsconfig for specific project types
  • Maintain strict TypeScript settings
  • Use proper path aliases for imports
  • Keep compiler options consistent across the monorepo

Turbo Configuration

  • Configure pipeline tasks efficiently
  • Define proper dependencies between tasks
  • Use caching effectively for improved build performance
  • Ensure proper dependencies are defined in package.json

GitHub Workflows

  • Follow consistent patterns for CI/CD workflows
  • Test across all target platforms
  • Configure proper caching for faster CI runs
  • Implement pre-merge checks for code quality

Code Generation

  • Document generator templates
  • Keep generators up to date with project conventions
  • Test generated code for quality and correctness
filepath applyTo
.instructions/typescript.instructions.md
**/*.ts,**/*.tsx

Project coding standards for TypeScript and React

Apply the general coding guidelines to all code.

TypeScript Guidelines

  • Use TypeScript for all new code
  • Follow functional programming principles where possible
  • Use interfaces for data structures and type definitions
  • Prefer immutable data (const, readonly)
  • Use optional chaining (?.) and nullish coalescing (??) operators
  • Ensure everything is type-safe and types are properly defined when needed
  • If you touch ts/tsx code, run get_errors immediately to quickly check for errors then fix errors

React Guidelines

  • Use functional components with hooks
  • Follow the React hooks rules (no conditional hooks)
  • Use React.FC type for components with children
  • Keep components small and focused
  • Use Tailwind/Nativewind CSS classNames for component styling
filepath applyTo
.instructions/apps/nextjs/nextjs.instructions.md
apps/nextjs/**/*

Next.js Application Guidelines

Project Structure

  • Use the App Router pattern for routing
  • Organize the application with these directories:
    • src/app/ - Page components and routes
    • src/app/_components/ - Shared components for the app
    • src/app/api/ - API route handlers
    • src/trpc/ - tRPC client configuration
    • src/auth/ - Auth client integration

Component Organization

  • Place route-specific components within route directories with _components folder:

    app/book/[conceptId]/_components/concept-stories-client.tsx
    
  • Name client components with -client suffix when they need to be client components:

    "use client";
    
    export function ConceptStoriesClient() {
      // Client-side component implementation
    }
  • Use Server Components by default, only use Client Components when necessary

  • Apply "use client" directive only when component requires:

    • Interactivity (event handlers)
    • Browser-only APIs
    • React hooks
    • Component state

Server and Client Components

  • Leverage React Server Components for data loading:

    // page.tsx (Server Component)
    export default async function Page() {
      const data = await fetchDataDirectly();
      return <ClientComponent initialData={data} />;
    }
  • Use Client Components for interactive elements:

    // component-client.tsx
    "use client";
    
    export function ComponentClient() {
      const [state, setState] = useState();
      // Interactive component code
    }

tRPC Integration

  • Use the provided tRPC provider in layouts:

    <TRPCReactProvider>
      {props.children}
    </TRPCReactProvider>
  • Configure API routes for tRPC in app/api/trpc/[trpc]/route.ts

  • Access tRPC procedures in Client Components:

    "use client";
    
    import { api } from "~/trpc/react";
    
    export function Component() {
      const { data } = api.someRouter.someQuery.useQuery();
    }

Data Fetching

  • Use React Server Components for data fetching wherever possible

  • Implement React Suspense for loading states:

    <Suspense fallback={<LoadingSkeleton />}>
      <DataComponent />
    </Suspense>
  • For client-side data fetching, use tRPC procedures from the API package:

    const { data, isLoading } = api.someRouter.someQuery.useQuery();

Authentication

  • Use the auth package for authentication:

    import { auth } from "@kidbook/auth/client";
  • Handle protected routes via middleware in middleware.ts:

    export default auth.middleware;
    
    export const config = {
      matcher: ["/protected/:path*"],
    };
  • Show different UI based on auth state:

    import { useAuth } from "@kidbook/auth/client";
    
    export function AuthButton() {
      const { session, signIn, signOut } = useAuth();
    
      return session ? (
        <button onClick={() => signOut()}>Sign Out</button>
      ) : (
        <button onClick={() => signIn.oAuth({ provider: "twitter" })}>
          Sign In
        </button>
      );
    }

Styling

  • Use Tailwind CSS with consistent class ordering using the cn utility:

    import { cn } from "@kidbook/ui";
    
    <div className={cn(
      "base-classes",
      conditional && "conditional-classes",
      props.className,
    )}>
  • Follow component design system from UI package

  • For complex components, use the cva utility from UI package:

    import { cva } from "class-variance-authority";
    
    const variants = cva("base", {
      variants: {
        size: { sm: "text-sm", md: "text-md", lg: "text-lg" },
      },
    });

Metadata and SEO

  • Configure metadata in layout.tsx:

    export const metadata: Metadata = {
      metadataBase: new URL("https://kidbook-unlimited.vercel.app"),
      title: "Kidbook",
      description: "A simple kid book app",
      // Additional metadata
    };
  • Use dynamic metadata in specific routes:

    export async function generateMetadata({ params }): Promise<Metadata> {
      const concept = await getConcept(params.conceptId);
    
      return {
        title: concept.title,
        description: concept.description,
      };
    }

Error Handling

  • Create error.tsx files for route-specific error handling:

    "use client";
    
    export default function Error({ error, reset }) {
      return (
        <div>
          <h2>Something went wrong!</h2>
          <button onClick={reset}>Try again</button>
        </div>
      );
    }
  • Implement loading.tsx files for route-specific loading states:

    export default function Loading() {
      return <div>Loading...</div>;
    }

Environment Variables

  • Access environment variables via the env module:

    import { env } from "~/env";
    
    // Use env.VARIABLE_NAME
  • Define environment variables with validation in env.ts:

    export const env = createEnv({
      server: {
        NODE_ENV: z.enum(["development", "test", "production"]),
      },
      // Client variables with runtimeEnv
    });
filepath applyTo
.instructions/apps/expo/expo.instructions.md
apps/expo/**/*

Expo Application Guidelines

Project Structure

  • Organize the application with these directories:
    • src/app/ - Screen components following Expo Router conventions
    • src/utils/ - Utility functions and configuration
    • src/styles.css - NativeWind styles

Routing and Navigation

  • Use Expo Router for file-based navigation:

    src/app/
      _layout.tsx          # Main layout wrapper
      index.tsx            # Home screen
      book/                # Nested route
        [conceptId]/       # Dynamic route
          index.tsx        # Concept detail screen
          [storyId]/       # Nested dynamic route
            index.tsx      # Story screen
    
  • Configure screen options in layout files:

    <Stack
      screenOptions={{
        contentStyle: {
          backgroundColor: colorScheme == "dark" ? "#09090B" : "#FFFFFF",
        },
      }}
    />

tRPC Integration

  • Use the provided tRPC provider in the root layout:

    <TRPCReactProvider getCookie={() => authClient.getCookie()}>
      {/* App components */}
    </TRPCReactProvider>
  • Access tRPC procedures in components:

    import { api } from "@kidbook/api/client";
    
    function Component() {
      const { data } = api.someRouter.someQuery.useQuery();
      // Component implementation
    }

Authentication

  • Use the auth client from utilities:

    import { authClient } from "~/utils/auth-client";
  • Integrate authentication with tRPC:

    <TRPCReactProvider getCookie={() => authClient.getCookie()}>
      {/* App components */}
    </TRPCReactProvider>
  • Handle authentication state:

    import { useAuth } from "~/utils/auth-client";
    
    function AuthComponent() {
      const { session, signIn, signOut } = useAuth();
    
      if (!session) {
        return <SignInButton onPress={() => signIn.oAuth({ provider: "twitter" })} />;
      }
    
      return <SignOutButton onPress={() => signOut()} />;
    }

Styling

  • Use NativeWind (Tailwind CSS for React Native):

    import { View, Text } from "react-native";
    
    <View className="flex-1 items-center justify-center bg-background">
      <Text className="text-foreground font-bold text-xl">Hello World</Text>
    </View>
  • Use the theme system with useColorScheme:

    import { useColorScheme } from "nativewind";
    
    function Component() {
      const { colorScheme, toggleColorScheme } = useColorScheme();
      // Use colorScheme to conditionally apply styles
    }
  • Import shared styles:

    import "../styles.css";

Component Patterns

  • Use React Native components with NativeWind styling:

    import { View, Text, Pressable } from "react-native";
    import { cn } from "@kidbook/ui";
    
    function Button({ label, onPress, className }) {
      return (
        <Pressable
          onPress={onPress}
          className={cn(
            "bg-primary px-4 py-2 rounded-md",
            className
          )}
        >
          <Text className="text-primary-foreground font-medium">
            {label}
          </Text>
        </Pressable>
      );
    }
  • Import and use shared UI components:

    import { Button } from "@kidbook/ui";
    
    function Screen() {
      return (
        <View className="p-4">
          <Button onPress={handlePress}>Press Me</Button>
        </View>
      );
    }

Data Fetching

  • Use tRPC procedures from the API package:

    const { data, isLoading, error } = api.concept.byId.useQuery({ id });
    
    if (isLoading) {
      return <LoadingIndicator />;
    }
    
    if (error) {
      return <ErrorDisplay message={error.message} />;
    }
    
    return <ConceptDetail concept={data} />;
  • Handle loading and error states explicitly:

    const { data, isLoading, error } = api.someResource.someQuery.useQuery();
    
    // Display appropriate UI for each state

Environment Configuration

  • Access base URLs and environment variables through utilities:

    import { getBaseUrl } from "~/utils/base-url";
    
    const apiUrl = `${getBaseUrl()}/api/resource`;

Deep Linking

  • Configure deep links for authentication and navigation in app.config.ts:

    // app.config.ts
    export default defineConfig({
      // ... other config
      scheme: "kidbook",
      // Define additional schemes for development
    });
  • Handle deep links for authentication:

    // Register deep link handler
    const subscription = registerAuthDeepLinks();
    
    // Clean up when component unmounts
    useEffect(() => {
      return () => subscription.remove();
    }, []);

Platform-Specific Behavior

  • Use platform checks when necessary:

    import { Platform } from "react-native";
    
    const padding = Platform.OS === "ios" ? 20 : 16;
  • Import platform-specific components:

    import { Button } from "@kidbook/ui"; // Will use the native version

Performance Considerations

  • Use list virtualization for long lists:

    import { FlatList } from "react-native";
    
    <FlatList
      data={items}
      keyExtractor={item => item.id}
      renderItem={({ item }) => <ItemComponent item={item} />}
    />
  • Memoize components to prevent unnecessary re-renders:

    import { memo } from "react";
    
    const MemoizedComponent = memo(function Component(props) {
      // Component implementation
    });
filepath applyTo
.instructions/packages/api/api.instructions.md
packages/api/**/*

API Package Guidelines

Directory Structure

  • /src/router/ - Contains domain-specific tRPC routers (concept, story, chapter, etc.)
  • /src/repositories/ - Contains database access functions organized by entity
  • /src/services/ - Contains business logic organized by domain and functionality
  • /src/trpc.ts - Core tRPC configuration and procedure definitions
  • /src/root.ts - Root router that combines all domain routers
  • /src/constants.ts - Shared constants across the API
  • /src/client/ - Client-side utilities and helpers for API consumption

tRPC Router Implementation

  • Create domain-specific routers in separate files under /router/:

    // router/concept.ts
    export const conceptRouter = createTRPCRouter({
      procedure1: publicProcedure...,
      procedure2: protectedProcedure...,
    });
  • Export the router for inclusion in the root router:

    // root.ts
    export const appRouter = createTRPCRouter({
      concept: conceptRouter,
      story: storyRouter,
      // other domain routers...
    });
  • Use proper procedure types:

    • publicProcedure - For endpoints that don't require authentication
    • protectedProcedure - For endpoints that require authenticated users
  • Follow this pattern for procedure definitions:

    procedureName: publicProcedure
      .input(z.object({...}))      // Input validation with Zod
      .output(outputSchema)        // Output validation with Zod
      .query(async ({ctx, input}) => {
        // Implementation using repository functions
      })

Repository Pattern

  • Create repository files for database access organized by entity:

    // repositories/concept.ts
    export async function getConceptById(
      ctx: { db: typeof db },
      conceptId: string,
    ): Promise<ConceptRow | undefined> {
      // Implementation...
    }
  • Use drizzle-orm for database operations:

    const results = await ctx.db
      .select()
      .from(concepts)
      .where(eq(concepts.id, conceptId))
      .limit(1);
  • Define proper return types:

    export type ConceptRow = InferModel<typeof concepts>;
    export interface ConceptWithImages extends ConceptRow {
      images: ImageRow[];
      cover: ImageRow;
    }
  • Implement pagination using cursor-based approach:

    const whereConditions = [];
    if (cursor) {
      whereConditions.push(lt(concepts.createdAt, new Date(cursor)));
    }
    // ...
    return { items, hasNext };

Service Organization

  • Group service functions by domain:

    /services/images/
    /services/chapter/
    
  • Split service files by functional concerns:

    generate.ts - Generation logic
    prompt.ts - Prompt construction
    tool.ts - Tool definitions for AI interactions
    
  • Pass context to services and repositories instead of importing directly:

    export const generateAndStream = async (
      ctx: { db: typeof db },
      chapterId: string,
      emit: (chunk: string) => void,
    ): Promise<void> => {
      // Use ctx.db instead of importing
    };

Type Safety

  • Use Zod for input and output validation:

    const inputSchema = z.object({
      id: z.string(),
      limit: z.number().min(1).max(100),
      cursor: z.string().nullish(),
    });
    
    const outputSchema = z.object({
      items: z.array(itemSchema),
      nextCursor: z.string().nullable(),
    });
  • Define interfaces for complex objects:

    export interface ConceptWithImages extends ConceptRow {
      images: ImageRow[];
      cover: ImageRow;
    }
  • Leverage drizzle-orm types:

    import type { InferModel } from "drizzle-orm";
    
    import { conceptSchema } from "@kidbook/db/schema";
    
    export type ConceptRow = InferModel<typeof concepts>;

Error Handling

  • Use TRPCError for API-level errors:

    if (!concept) {
      throw new TRPCError({
        code: "NOT_FOUND",
        message: "Concept not found",
      });
    }
  • Implement try/catch blocks in repositories:

    try {
      const results = await ctx.db...
      return results;
    } catch (error) {
      console.error(
        "Error message:",
        error instanceof Error ? error.message : String(error),
      );
      return []; // Or appropriate default
    }
  • Use meaningful error codes:

    • NOT_FOUND - Resource doesn't exist
    • UNAUTHORIZED - User not authenticated
    • FORBIDDEN - User not authorized
    • BAD_REQUEST - Invalid input

AI Integration

  • Use AI SDK for OpenAI interactions:

    import { openai } from "@ai-sdk/openai";
    import { streamText, tool } from "ai";
  • Structure AI prompts with proper context:

    const stream = streamText({
      model: openai("gpt-4.1"),
      prompt: systemPrompt,
      maxSteps: 5,
      tools: {...}
    });
  • Define specialized tools:

    addImage: tool({
      parameters: z.object({...}),
      execute: async (input) => {...}
    })

Performance Considerations

  • Use cursor-based pagination:

    if (cursor) {
      whereConditions.push(lt(concepts.createdAt, new Date(cursor)));
    }
  • Implement proper SQL queries with appropriate filters:

    .orderBy(desc(concepts.createdAt))
    .limit(limit + 1);
  • Use artificial delay for development to simulate network conditions:

    if (t._config.isDev) {
      // artificial delay in dev 100-500ms
      const waitMs = Math.floor(Math.random() * 400) + 100;
      await new Promise((resolve) => setTimeout(resolve, waitMs));
    }

Authentication & Authorization

  • Use the auth package for authentication
  • Implement proper authorization checks in procedures
  • Create protected procedures where necessary

Best Practices

  • Keep procedures focused on a single responsibility
  • Document complex procedures with comments
  • Use middleware for cross-cutting concerns
  • Handle rate limiting for public-facing endpoints
filepath applyTo
.instructions/packages/auth/auth.instructions.md
packages/auth/**/*

Auth Package Guidelines

Structure

  • auth.ts - Main authentication configuration and initialization
  • auth-client.ts - Web client integration
  • auth-client-expo.ts - Expo/mobile client integration

Authentication Framework

  • Use Better Auth as the authentication framework
  • Configure with Drizzle adapter for database integration:
    export const auth = betterAuth({
      database: drizzleAdapter(db, {
        provider: "pg",
      }),
      // Additional configuration...
    });

Authentication Methods

  • Email and password authentication:

    emailAndPassword: { enabled: true },
  • Social providers (configured in auth.ts):

    socialProviders: {
      twitter: {
        clientId: env.AUTH_TWITTER_ID,
        clientSecret: env.AUTH_TWITTER_SECRET,
        redirectURI: env.NODE_ENV === "production"
          ? "https://kidbook-unlimited.vercel.app/api/auth/callback/twitter"
          : undefined,
      },
      // Additional providers can be added here
    },

Platform Integration

  • For Next.js web app:
    • Use auth-client.ts which handles web-specific auth flows
    • Import as: import { auth } from "@kidbook/auth/client"
  • For Expo mobile app:
    • Use auth-client-expo.ts which handles native auth flows
    • Import as: import { auth } from "@kidbook/auth/client-expo"

Plugin Configuration

  • Use the OAuth proxy plugin for handling OAuth flows:

    plugins: [
      oAuthProxy(),
      expo(), // For Expo support
      // Additional plugins as needed
    ],
  • Configure trusted origins to allow deep linking:

    trustedOrigins: [
      "exp://",
      "exp+kidbook://",
      "https://localhost:3000",
      "kidbook://",
      "https://appleid.apple.com",
    ],

Session Management

  • Export the Session type for use in API and components:

    export type Session = NonNullable<
      Awaited<ReturnType<typeof auth.api.getSession>>
    >;
  • Access session in API routes:

    const session = await auth.api.getSession({ headers: request.headers });
    if (!session?.user) {
      // Handle unauthenticated user
    }
  • Use client hooks for session state in components:

    const { session, signIn, signOut } = useAuth();
    if (session?.user) {
      // User is authenticated
    }

Environment Variables

  • Required environment variables:
    • AUTH_TWITTER_ID - Twitter OAuth client ID
    • AUTH_TWITTER_SECRET - Twitter OAuth client secret
    • Additional variables for other providers as needed

Client Usage

  • Web client functions:

    // Sign in with email/password
    await auth.signIn.emailPassword({ email, password });
    
    // Sign in with OAuth provider
    await auth.signIn.oAuth({ provider: "twitter" });
    
    // Sign out
    await auth.signOut();
  • Expo client functions:

    // Sign in with OAuth provider in Expo
    await auth.signIn.oAuth({ provider: "twitter" });
    
    // Handle deep links for OAuth callback
    useEffect(() => {
      // Register deep link handler
      const subscription = registerAuthDeepLinks();
      return () => subscription.remove();
    }, []);

Protecting Routes

  • In Next.js, use middleware for route protection:

    // middleware.ts
    import { auth } from "@kidbook/auth/client";
    
    export default auth.middleware;
    
    // Specify which routes to protect
    export const config = {
      matcher: ["/dashboard/:path*", "/settings/:path*"],
    };
  • In Expo, use conditional rendering:

    function ProtectedScreen() {
      const { session, isLoading } = useAuth();
    
      if (isLoading) return <LoadingView />;
      if (!session?.user) return <SignInScreen />;
    
      return <ProtectedContent />;
    }

Authentication Flow

  • Implement secure authentication flows using appropriate providers
  • Support multiple authentication methods (email/password, OAuth, etc.)
  • Manage session state consistently across platforms

Security Best Practices

  • Use proper password hashing and salting
  • Implement CSRF protection
  • Follow OAuth 2.0 best practices
  • Enforce proper session management and expiration

User Management

  • Implement proper user creation, updating, and deletion
  • Handle account verification workflows
  • Support password reset functionality

Authorization

  • Implement role-based access control
  • Support fine-grained permissions
  • Ensure proper authorization checks in protected resources

Cross-Platform Support

  • Ensure authentication works consistently across web and mobile
  • Handle platform-specific authentication requirements
  • Use secure storage for tokens on all platforms

Error Handling

  • Provide clear error messages for authentication failures
  • Handle edge cases like expired tokens, invalid credentials
  • Implement proper logging for security events

Testing

  • Write unit tests for authentication logic
  • Implement integration tests for auth flows
  • Use mock providers for testing

Maintenance

  • Keep authentication libraries up to date
  • Monitor for security vulnerabilities
  • Document any changes to authentication flows
filepath applyTo
.instructions/packages/db/db.instructions.md
packages/db/**/*

Database Package Guidelines

Directory Structure

  • /src/schemas/ - Individual schema files organized by domain
  • /src/client.ts - Database client configuration
  • /src/schema.ts - Barrel file for combined schema exports
  • /src/index.ts - Package entrypoint
  • /drizzle/ - Generated migrations

Table Definitions

  • Define tables using pgTable with proper typing:

    export const concepts = pgTable("concepts", (t) => ({
      id: t.uuid().notNull().primaryKey().defaultRandom(),
      genre: t.text("genre").notNull(),
      title: t.text("title").notNull(),
      short_pitch: t.text("short_pitch").notNull(),
      long_pitch: t.text("long_pitch").notNull(),
      authors_guide: t.text("authors_guide").notNull(),
      status: conceptStatusEnum("status").notNull().default("drafting"),
      createdAt: t
        .timestamp({ mode: "date", withTimezone: true })
        .notNull()
        .defaultNow(),
      updatedAt: t
        .timestamp({ mode: "date", withTimezone: true })
        .notNull()
        .$onUpdateFn(() => new Date()),
    }));
  • Use enums for known value sets:

    export const conceptStatusEnum = pgEnum("concept_status", [
      "drafting",
      "testing",
      "locked",
    ]);
  • Follow these naming conventions:

    • Table names: plural nouns, snake_case in database (camelCase in code)
    • Column names: camelCase in code, snake_case in database (handled by config)
    • Primary keys: singular table name + 'Id' (e.g., storyId for stories table)
    • Foreign keys: same as the primary key they reference (e.g., conceptId)

Relations

  • Define all relations in a central relations.ts file to avoid circular dependencies:

    export const storiesRelations = relations(stories, ({ one, many }) => ({
      concept: one(concepts, {
        fields: [stories.conceptId],
        references: [concepts.id],
      }),
      user: one(user, {
        fields: [stories.userId],
        references: [user.id],
      }),
      chapters: many(chapters),
    }));
  • For polymorphic relations, use this pattern:

    // Use reference type + reference id pattern
    export const images = pgTable("images", (t) => ({
      referenceType: imageReferenceTypeEnum("reference_type").notNull(),
      referenceId: t.uuid("reference_id").notNull(),
      // ...other fields
    }));
    
    // Then set up basic relations
    export const imagesRelations = relations(images, ({ one }) => ({
      concept: one(concepts, {
        fields: [images.referenceId],
        references: [concepts.id],
      }),
      // ...other relations
    }));
    
    // Apply filters when querying
    // db.query.images.findMany({
    //   where: eq(images.referenceType, "concept"),
    //   with: { concept: true }
    // })

Schema Organization

  • Group tables by domain in separate files:

    • auth-schema.ts
    • concept-schema.ts
    • story-schema.ts
    • etc.
  • Export schemas through the barrel file:

    // schema.ts
    export const conceptSchema = {
      concepts,
    };
    
    export const storySchema = {
      stories,
      characters,
      chapters,
    };

Client Configuration

  • Use Vercel Postgres with drizzle:

    import { sql } from "@vercel/postgres";
    import { drizzle } from "drizzle-orm/vercel-postgres";
    
    export const db = drizzle({
      client: sql,
      schema: {
        ...authSchema,
        ...postSchema,
        // ...other schemas
        ...relations,
      },
      casing: "snake_case", // Handle casing conversion
    });

Timestamps and Auditing

  • Include consistent timestamp fields on all tables:
    createdAt: t
      .timestamp({ mode: "date", withTimezone: true })
      .notNull()
      .defaultNow(),
    updatedAt: t
      .timestamp({ mode: "date", withTimezone: true })
      .notNull()
      .$onUpdateFn(() => new Date()),

Migrations

  • Configure drizzle-kit for migrations:

    // drizzle.config.ts
    export default {
      schema: "./src/schemas/*.ts",
      dialect: "postgresql",
      dbCredentials: { url: process.env.DATABASE_URL },
      casing: "snake_case",
    } satisfies Config;
  • Generate and Apply migrations:

    pnpm db:generate
    pnpm db:migrate
    

    on dev we can just run:

    pnpm db:push
    

Best Practices

  • Use appropriate column types:

    • UUIDs for IDs: t.uuid().defaultRandom()
    • Text for variable-length strings: t.text()
    • JSON for structured data: t.json()
    • Timestamps with timezone: t.timestamp({ mode: "date", withTimezone: true })
    • Enums for constrained values: pgEnum() + column using that enum
  • Enforce constraints at database level:

    • Not null: .notNull()
    • Defaults: .default()
    • Foreign keys: Define in relations
    • Unique constraints: .unique()
  • Add indexes for frequently queried fields:

    // Example: add index for a foreign key
    conceptId: t.uuid("concept_id").references(() => concepts.id),
      // Index definition
      { name: "idx_stories_concept_id", columns: [stories.conceptId] };

Schema Design

  • Use Drizzle ORM for database schema definition and operations
  • Define tables with proper relationships
  • Use appropriate data types for columns
  • Implement proper indexes for frequently queried fields

Queries

  • Write optimized queries to avoid performance issues
  • Use transactions for operations that need atomicity
  • Implement proper pagination for large result sets

Relationships

  • Define relationships using proper foreign key constraints
  • Use join operations efficiently
  • Consider denormalization for performance-critical queries

Type Safety

  • Leverage TypeScript and Drizzle's type inference
  • Ensure database types are properly mapped to application types
  • Use branded types for IDs where appropriate

Error Handling

  • Handle database errors gracefully
  • Implement retry logic for transient failures
  • Use custom error types for database-specific errors

Security

  • Never store sensitive data in plain text
  • Implement proper access controls
  • Use parameterized queries to prevent SQL injection
filepath applyTo
.instructions/packages/ui/ui.instructions.md
packages/ui/**/*

UI Package Guidelines

File Structure & Platform-Specific Components

  • Use .web.tsx and .native.tsx suffixes for platform-specific implementations
  • ONLY EVER Export components through the main index.web.ts and index.native.ts barrel files. NO OTHER BARRELS IN THIS PACKAGE.
  • Group related components in subdirectories (e.g., /components/button/)
  • Define shared types in separate .types.ts files (e.g., story.types.ts)
  • Keep related components together (e.g., all story components in the same directory)

Component Implementation Patterns

  • Use function components with explicit return types
  • Export both the component and its variants when using class-variance-authority:
    export const buttonVariants = cva("...", {...});
    export function Button({...}: ButtonProps) {...}
  • Implement prop interfaces that extend native element props:
    interface ButtonProps extends React.ComponentProps<"button">, VariantProps<typeof buttonVariants> {...}
  • Use the asChild pattern for web components (from Radix UI) to allow composition
  • Always provide default values for optional props

Cross-Platform Compatibility

  • Maintain API compatibility between web and native versions
  • If using "useState" or "useEffect" (etc) in web components you need "use client" at the top of the .web file to be compatibile with nextjs15
  • Simulate web patterns in native (e.g., class-variance-authority pattern for styling)
  • Use the same prop interfaces with platform-specific adjustments:
    // Web: React.ComponentProps<"button">
    // Native: React.ComponentProps<typeof Pressable>
  • Support both onClick (web) and onPress (native) in native components for API consistency

Styling Conventions

  • Use NativeWind/Tailwind classes via the className prop

  • Use the cn() utility from utils.ts to merge Tailwind classes

  • Follow this structure for class-variance-authority components:

    const buttonVariants = cva(
      "base-classes-here",s
      {
        variants: { variant: {...}, size: {...} },
        defaultVariants: { variant: "primary", size: "md" }
      }
    );
  • Use consistent naming for variants across components (primary, secondary, destructive, etc.)

  • Use consistent size names (sm, md, lg) across all components

  • Use the project's design system colors through CSS variables. You should use these instead of generic whites, greys, and blacks as they are theme aware.

    bg-primary text-primary-foreground /* Primary button */
    bg-secondary text-secondary-foreground /* Secondary button */
    bg-destructive text-destructive-foreground /* Destructive button */
    bg-muted text-muted-foreground /* Subtle UI elements */
    bg-accent text-accent-foreground /* Highlighted UI elements */
    bg-card text-card-foreground /* Card component */
    bg-background text-foreground /* Base colors */
    bg-border text-border /* Form control colors */
    ... etc
  • Follow these opacity patterns for hover/focus states:

    hover:bg-primary/90 /* 90% opacity for hover */
    active:opacity-90 /* 90% opacity for active state */
  • Use the cn() utility from utils.ts to merge Tailwind classes

  • Use these utility classes for scrollbar styling:

    .no-scrollbar /* Hides scrollbars across browsers */

Data Fetching Patterns

  • Import hooks from their respective packages:

    import {
      useQuery,
      useQueryClient,
      useSuspenseQuery,
    } from "@tanstack/react-query";
    
    import { useTRPC } from "@kidbook/api/client";
  • Use useTRPC() to access tRPC procedures:

    const trpc = useTRPC();
    const { data } = useQuery(
      trpc.resourceName.procedureName.queryOptions({ params }),
    );
  • Use useSuspenseQuery() for components wrapped in Suspense boundaries:

    const { data: concept } = useSuspenseQuery(
      trpc.concept.byId.queryOptions({ id }),
    );
  • Implement Suspense boundaries for components that fetch data:

    <Suspense fallback={<ComponentSkeleton />}>
      <DataFetchingComponent id={id} />
    </Suspense>
  • Create skeleton components for loading states:

    function ComponentSkeleton() {
      return (
        <div className="animate-pulse">
          {/* Skeleton UI elements */}
        </div>
      );
    }
  • Use React Query's useQueryClient() for cache manipulation:

    const queryClient = useQueryClient();
    // Later: queryClient.invalidateQueries({ queryKey: [...] });

Type Safety

  • Import types from API package using RouterOutputs type
  • Define prop interfaces with clear TypeScript types
  • Use branded or domain-specific types when appropriate
  • Export component variants for use in consuming applications
  • Use React.ReactNode for children unless a more specific type is needed

Component API Patterns

  • Keep consistent prop naming across similar components:
    • className for style customization
    • variant for visual variants
    • size for size variations
    • asChild for composition (web only)
    • onClick/onPress for interaction handlers
  • Add event handling props appropriate for the component type
  • Use the children prop for component composition
  • Allow slot pattern where appropriate via asChild in web components

Icon Usage Example

Use react-feather icons for web components and react-native-unified-icons for native ones. The BackButton component illustrates this pattern:

"use client";

import { useRouter } from "next/navigation";
import { CornerUpLeft } from "react-feather";
import { Button } from "../button/button.web";

export interface BackButtonProps {
  className?: string;
}

export function BackButton({ className }: BackButtonProps) {
  const router = useRouter();
  return (
    <Button className={className} size="sm" onClick={() => router.back()}>
      <CornerUpLeft className="mr-2 h-4 w-4" aria-hidden="true" />
      Back
    </Button>
  );
}

The native implementation follows the same API but uses onPress and the Icon component to render icons:

import { useRouter } from "expo-router";
import { Icon } from "react-native-unified-icons";
import { Button } from "../button/button.native";

export interface BackButtonProps {
  className?: string;
}

export function BackButton({ className }: BackButtonProps) {
  const router = useRouter();
  return (
    <Button className={className} size="sm" onPress={() => router.back()}>
      <Icon
        select={{ from: "feather", name: "corner-up-left" }}
        size={16}
        color="white"
        className="mr-2"
      />
      Back
    </Button>
  );
}

Icons should include aria-hidden="true" when decorative and be given h-/w- classes for consistent sizing.

Component Organization

  • Categorize components as UI primitives, domain components, or layout components
  • Group related components (e.g., all story-related components together)
  • Maintain a clear hierarchy of composition (simple components used in more complex ones)
  • Use context providers for state management where appropriate
filepath applyTo
.instructions/packages/validators/validators.instructions.md
packages/validators/**/*

Validators Package Guidelines

Purpose and Usage

  • Use this package for shared validation schemas between frontend and backend
  • Currently, simple schemas come from drizzle-zod in the DB package
  • Use this package for more complex validation logic that goes beyond database schemas

Schema Definition Pattern

  • Define schemas using Zod:

    import { z } from "zod";
    
    export const userSchema = z.object({
      name: z.string().min(2).max(50),
      email: z.string().email(),
      role: z.enum(["user", "admin"]).default("user"),
    });
  • Export inferred types from schemas:

    import { z } from "zod";
    
    export const postCreateSchema = z.object({
      title: z.string().min(1).max(100),
      content: z.string().min(10),
      published: z.boolean().default(false),
    });
    
    export type PostCreate = z.infer<typeof postCreateSchema>;

Naming Conventions

  • Use descriptive, consistent naming for schemas
  • Follow the pattern: entityNameSchema for main schemas
  • Use entityNameAction for operation-specific schemas:
    export const userSchema = z.object({...}); // Complete user schema
    export const userCreateSchema = z.object({...}); // Schema for user creation
    export const userUpdateSchema = z.object({...}); // Schema for user updates

Complex Validators

  • Create reusable validation components:

    // Reusable password validator
    export const passwordValidator = z
      .string()
      .min(8)
      .regex(/[A-Z]/, "Password must contain at least one uppercase letter")
      .regex(/[a-z]/, "Password must contain at least one lowercase letter")
      .regex(/[0-9]/, "Password must contain at least one number");
    
    // Use in other schemas
    export const userCreateSchema = z.object({
      email: z.string().email(),
      password: passwordValidator,
    });
  • Implement custom validators for complex rules:

    export const dateRangeSchema = z
      .object({
        start: z.date(),
        end: z.date(),
      })
      .refine(({ start, end }) => end > start, {
        message: "End date must be after start date",
      });

API Integration

  • Import validators in API procedures:

    // In API router
    import { userCreateSchema } from "@kidbook/validators";
    
    export const userRouter = createTRPCRouter({
      create: publicProcedure
        .input(userCreateSchema)
        .mutation(async ({ ctx, input }) => {
          // Implementation using validated input
        }),
    });

Form Integration

  • Use validators with form libraries:

    // In a React component with React Hook Form
    import { zodResolver } from "@hookform/resolvers/zod";
    import { useForm } from "react-hook-form";
    
    import { userCreateSchema } from "@kidbook/validators";
    
    function SignupForm() {
      const form = useForm({
        resolver: zodResolver(userCreateSchema),
      });
      // Form implementation
    }

When To Use This Package

  • For validation logic needed in multiple places
  • For complex validation rules beyond simple database schema validation
  • When the same validation needs to be performed on both client and server
  • For form validation in the UI that matches API expectations

Exports

  • Export both schemas and inferred types:

    // Schema for validation
    export const storySchema = z.object({...});
    
    // Type for TypeScript usage
    export type Story = z.infer<typeof storySchema>;

Type Safety

  • Export inferred types from schemas using z.infer<typeof schemaName>
  • Use these types throughout the codebase for consistency
  • Maintain proper TypeScript integration

Validation Logic

  • Keep validation logic focused and specific
  • Implement custom validators for complex rules
  • Use appropriate error messages for validation failures

Error Handling

  • Provide clear, user-friendly error messages
  • Use consistent error format across all validators
  • Support internationalization for error messages when needed

Reusability

  • Create composable validation schemas
  • Avoid duplication by reusing schema components
  • Share common validation patterns across the application

Testing

  • Test validators with valid and invalid inputs
  • Verify error messages are correct and helpful
  • Ensure edge cases are properly handled

Documentation

  • Document complex validation rules
  • Include examples of valid and invalid values
  • Keep documentation updated when validation rules change
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment