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.
Route Structure
Example: Page with Prefetching
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
Feature Module Organization
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
View Component Pattern
Data Constants
Feature Hook
// 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.
Router Structure
Root Router
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
Database Client
R2 Storage
// 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
Update Database Schema
Edit prisma/schema.prisma, then: npx prisma migrate dev
npx prisma generate
Add New Feature
Create src/features/[feature-name]/
Add components, hooks, views
Create tRPC router if needed
Add route in src/app/
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