Skip to main content
Resonance follows a feature-based architecture with clear separation of concerns. This guide covers the project structure, key directories, and architectural patterns.

Directory Overview

The source code is organized into distinct layers, each serving a specific purpose:
src/
├── app/              # Next.js App Router pages and API routes
├── features/         # Feature modules (domain-driven)
├── trpc/             # tRPC routers and API layer
├── lib/              # Shared utilities and clients
├── components/       # Shared UI components
├── hooks/            # Global custom hooks
└── types/            # TypeScript type definitions

App Directory (src/app/)

Next.js 14 App Router structure with route groups and layouts.
app/
├── (dashboard)/              # Dashboard route group
   ├── page.tsx              # Main dashboard
   ├── layout.tsx            # Shared dashboard layout
   ├── text-to-speech/
   ├── page.tsx          # TTS main page
   └── [generationId]/   # Dynamic generation detail
   └── voices/
       └── page.tsx          # Voice library
├── api/
   ├── trpc/[trpc]/          # tRPC endpoint
   ├── audio/[generationId]/ # Audio streaming
   └── voices/
       ├── create/           # Voice upload endpoint
       └── [voiceId]/        # Voice retrieval
├── sign-in/[[...sign-in]]/
├── sign-up/[[...sign-up]]/
└── org-selection/
Route Groups: The (dashboard) directory is a route group that shares a common layout without affecting the URL structure.

Features Directory (src/features/)

Features are self-contained modules following domain-driven design principles. Each feature contains all related code.

Feature Structure

Each feature follows a consistent structure:
features/[feature-name]/
├── components/     # Feature-specific components
├── hooks/          # Feature-specific hooks
├── views/          # Page-level components
├── data/           # Constants, mock data, configs
├── contexts/       # React contexts (optional)
└── lib/            # Feature utilities (optional)
Example: The text-to-speech feature:
text-to-speech/
├── components/
│   ├── text-to-speech-form.tsx
│   ├── generate-button.tsx
│   ├── settings-panel.tsx
│   └── history-drawer.tsx
├── views/
│   └── text-to-speech-view.tsx
├── data/
│   └── constants.ts          # TEXT_MAX_LENGTH, etc.
├── hooks/
│   └── use-generation.ts
└── contexts/
    └── tts-context.tsx

Current Features

text-to-speech

TTS generation interface, settings, and history management

voices

Voice library, upload, preview, and categorization

dashboard

Home page, quick actions, and navigation

billing

Subscription management and usage tracking

Feature Best Practices

// features/[feature]/views/feature-view.tsx
// Views are "smart" components that connect data to UI

import { FeatureForm } from "../components/feature-form";
import { useFeatureData } from "../hooks/use-feature-data";

export function FeatureView({ initialValues }: Props) {
  const { data, isLoading } = useFeatureData();
  
  return (
    <div>
      <FeatureForm data={data} loading={isLoading} />
    </div>
  );
}

tRPC Layer (src/trpc/)

Type-safe API layer using tRPC v11. All client-server communication goes through tRPC procedures.
trpc/
├── init.ts                # tRPC context and procedures
├── query-client.ts        # React Query configuration
└── routers/
    ├── _app.ts            # Root router (exports AppRouter type)
    ├── voices.ts          # Voice operations
    ├── generations.ts     # TTS generation operations
    └── billing.ts         # Subscription operations

Procedure Types

Resonance uses three procedure types defined in src/trpc/init.ts:
Public procedure with Sentry middleware. No authentication required.
export const baseProcedure = t.procedure.use(sentryMiddleware);
Requires authenticated user. Provides userId in context.
export const authProcedure = baseProcedure.use(async ({ next }) => {
  const { userId } = await auth();
  
  if (!userId) {
    throw new TRPCError({ code: "UNAUTHORIZED" });
  }
  
  return next({ ctx: { userId } });
});
Requires authenticated user with organization. Provides userId and orgId in context.
export const orgProcedure = baseProcedure.use(async ({ next }) => {
  const { userId, orgId } = await auth();
  
  if (!userId) {
    throw new TRPCError({ code: "UNAUTHORIZED" });
  }
  
  if (!orgId) {
    throw new TRPCError({
      code: "FORBIDDEN",
      message: "Organization required",
    });
  }
  
  return next({ ctx: { userId, orgId } });
});
Most routers use orgProcedure since Resonance is organization-scoped.

Lib Directory (src/lib/)

Shared utilities and service clients used across features.
lib/
├── db.ts                    # Prisma client singleton
├── r2.ts                    # Cloudflare R2 storage client
├── chatterbox-client.ts     # TTS API client
├── polar.ts                 # Polar billing client
├── env.ts                   # Environment validation
└── utils.ts                 # Utility functions
// src/lib/db.ts
import { PrismaClient } from "@/generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
import { env } from "./env";

const adapter = new PrismaPg({
  connectionString: env.DATABASE_URL,
});

const globalForPrisma = global as unknown as { prisma: PrismaClient };
const prisma = globalForPrisma.prisma || new PrismaClient({ adapter });

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

export { prisma };

Components Directory (src/components/)

Shared components used across multiple features.
components/
├── ui/              # shadcn/ui components (button, card, etc.)
├── voice-avatar/    # Shared voice avatar component
└── page-header.tsx  # Reusable page header
Feature-specific components should live in features/[feature]/components/. Only truly shared components belong in src/components/.

Key Architectural Patterns

1. Server-Side Prefetching

Pages prefetch data on the server for instant hydration:
// In page.tsx (Server Component)
prefetch(trpc.voices.getAll.queryOptions());

return (
  <HydrateClient>
    <ClientView />
  </HydrateClient>
);

2. Feature Isolation

Features are self-contained with minimal cross-feature dependencies:
features/voices/     # Can import from lib/, components/, trpc/
  └── components/    # Cannot import from other features/

3. Type Safety

tRPC provides end-to-end type safety:
// Client automatically knows response type
const { data } = trpc.voices.getAll.useQuery();
//    ^? { custom: Voice[], system: Voice[] }

4. Error Boundaries

Global error handling with Sentry integration:
// src/app/global-error.tsx
export default function GlobalError({ error }: { error: Error }) {
  // Sentry captures errors automatically
}

Development Workflow

1

Start Development Server

npm run dev
2

Update Database Schema

Edit prisma/schema.prisma, then:
npx prisma migrate dev
npx prisma generate
3

Add New Feature

  1. Create src/features/[feature-name]/
  2. Add components, hooks, views
  3. Create tRPC router if needed
  4. Add route in src/app/
4

Type Check

npm run type-check

File Naming Conventions

  • Components: kebab-case.tsx (e.g., voice-selector.tsx)
  • Hooks: use-[name].ts (e.g., use-audio-playback.ts)
  • Views: [feature]-view.tsx (e.g., text-to-speech-view.tsx)
  • API Routes: route.ts (Next.js convention)
  • tRPC Routers: [resource].ts (e.g., voices.ts)

Import Aliases

The project uses TypeScript path aliases for clean imports:
import { trpc } from "@/trpc/client";           // src/trpc/client
import { prisma } from "@/lib/db";              // src/lib/db
import { Button } from "@/components/ui/button"; // src/components/ui/button
import type { Voice } from "@/generated/prisma/client";
Prisma client is generated to src/generated/prisma to keep generated code separate from source.

Next Steps

Database Schema

Learn about Prisma models and relationships

Extending Features

Add new voices, routers, and TTS parameters

Build docs developers (and LLMs) love