This document provides guidance for AI assistants working with the Fresco codebase.
Fresco is a web-based interview platform that brings Network Canvas interviews to the browser. It's built with Next.js 14 (App Router), TypeScript, and PostgreSQL. Version 3.0.0.
Documentation: https://documentation.networkcanvas.com/en/fresco
# Development
pnpm install # Install dependencies
pnpm dev # Start dev server (auto-starts PostgreSQL via Docker)
pnpm storybook # Component library at :6006
# Quality Checks
pnpm lint # ESLint
pnpm ts-lint # TypeScript type checking
pnpm test # Vitest unit tests
pnpm knip # Find unused code
# Build
pnpm build # Production build
pnpm build:platform # Full build with DB setup (Vercel)app/ # Next.js App Router pages and API routes
├── (blobs)/ # Setup & authentication (route group)
├── (interview)/ # Interview interface (route group)
├── dashboard/ # Admin dashboard pages
├── api/ # API endpoints
└── reset/ # Password reset
actions/ # Server Actions (Next.js)
components/ # React components
├── ui/ # shadcn/ui base components
├── data-table/ # Table components
└── layout/ # Layout components
lib/ # Core business logic
├── interviewer/ # Interview session management (Redux)
├── network-exporters/ # Data export functionality
└── network-query/ # Network analysis utilities
hooks/ # Custom React hooks
queries/ # Database query functions
schemas/ # Zod validation schemas
types/ # TypeScript type definitions
utils/ # Utility functions
prisma/ # Database schema
styles/ # Global CSS/SCSS
- Framework: Next.js 14.2 with App Router
- Language: TypeScript 5.8 (strict mode)
- Database: PostgreSQL with Prisma ORM
- Auth: Lucia authentication
- Styling: Tailwind CSS 4.1 + shadcn/ui
- State Management: Redux Toolkit (interview sessions)
- Forms: React Hook Form + Zod validation
- Package Manager: pnpm 9.1.1
- Strict mode enabled with
noUncheckedIndexedAccess - Use
typefor type definitions (notinterface) - enforced by ESLint - Prefer inline type imports:
import { type Foo } from './bar' - Unused variables must start with underscore:
_unusedVar - Path alias:
~/maps to project root
// Correct
import { type Protocol } from '@prisma/client';
import { cn } from '~/utils/shadcn';
// Type definition
export type CreateInterview = {
participantIdentifier?: string;
protocolId: string;
};- Never use
process.envdirectly - ESLint will error - Import from
~/env.jswhich validates with Zod:
import { env } from '~/env.js';
const dbUrl = env.DATABASE_URL;no-consoleESLint rule is enforced- Must disable ESLint for intentional logs:
// eslint-disable-next-line no-console
console.log('Debug info');Located in /actions/. Pattern:
- Mark with
'use server'directive - Use
requireApiAuth()for authentication - Return
{ error, data }pattern - Use
safeRevalidateTag()for cache invalidation - Track events with
addEvent()for activity feed
'use server';
import { requireApiAuth } from '~/utils/auth';
import { safeRevalidateTag } from '~/lib/cache';
import { prisma } from '~/utils/db';
export async function deleteItem(id: string) {
await requireApiAuth();
try {
const result = await prisma.item.delete({ where: { id } });
safeRevalidateTag('getItems');
return { error: null, data: result };
} catch (error) {
return { error: 'Failed to delete', data: null };
}
}- Server Components by default
- Use
'use client'directive only when needed - Authenticate with
requirePageAuth()orrequireAppNotExpired() - Wrap async operations in
<Suspense>
import { requirePageAuth } from '~/utils/auth';
export default async function DashboardPage() {
await requirePageAuth();
return (
<Suspense fallback={<Loading />}>
<AsyncComponent />
</Suspense>
);
}Using shadcn/ui with Tailwind. Follow the pattern:
- Use
cva(class-variance-authority) for variants - Use
cn()utility from~/utils/shadcnfor class merging - Export component + variants + skeleton when applicable
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '~/utils/shadcn';
const buttonVariants = cva('base-classes', {
variants: {
variant: { default: '...', destructive: '...' },
size: { default: '...', sm: '...' },
},
defaultVariants: {
variant: 'default',
size: 'default',
},
});
export type ButtonProps = {
variant?: VariantProps<typeof buttonVariants>['variant'];
} & React.ButtonHTMLAttributes<HTMLButtonElement>;Use the custom useZodForm hook:
import useForm from '~/hooks/useZodForm';
import { z } from 'zod';
const schema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
function MyForm() {
const form = useForm({ schema });
// ...
}- Schema at
prisma/schema.prisma - Client imported from
~/utils/db - Uses connection pooling (DATABASE_URL) and direct connection (DATABASE_URL_UNPOOLED)
- Key models: User, Protocol, Interview, Participant, AppSettings, Events
import { prisma } from '~/utils/db';
const interviews = await prisma.interview.findMany({
where: { protocolId },
include: { participant: true },
});- Files: PascalCase for components (
Button.tsx), camelCase for utils (shadcn.ts) - Schemas: Located in
/schemas/, use Zod, prefix types with schema name - Hooks: camelCase starting with
use(useZodForm.ts) - Actions: camelCase functions (
deleteInterviews) - Routes: kebab-case folders matching URL paths
Prettier configuration (.prettierrc):
- Single quotes
- 80 character print width
- Tailwind class sorting plugin
ESLint:
- TypeScript strict type checking
- Next.js Core Web Vitals
- No unused variables (except
_prefix) - Consistent type imports
- Unit Tests: Vitest with React Testing Library
- E2E Tests: Playwright (via GitHub Actions)
- Visual Tests: Storybook with Chromatic
- Load Tests: k6 (
pnpm load-test)
Run tests:
pnpm test # Unit tests
pnpm storybook # Component testingfresco.config.ts- App-specific constants (protocol extensions, timeouts)env.js- Environment variable validationnext.config.js- Next.js configurationcomponents.json- shadcn/ui configuration.nvmrc- Node.js version (20)docker-compose.dev.yml- Development database
- Create function in
/actions/ - Add
'use server'directive - Add auth check with
requireApiAuth() - Define input types in
/schemas/ - Invalidate cache with
safeRevalidateTag()
- Create folder in
/app/following route structure - Add
page.tsx(Server Component) - Add auth check at top
- Use existing layout components from
/components/layout/
- Check if shadcn/ui has it first
- Add to
/components/ui/ - Use
cvafor variants - Export types and skeleton variant
- Define Zod schema in
/schemas/ - Use
useZodFormhook - Connect to React Hook Form
- Handle server action submission
- Main branch protection likely enabled
- Dependabot configured for security updates
- GitHub Actions for CI/CD (build checks, Playwright tests, Docker publishing)
- No direct
process.env- use~/env.js - No
console.logwithout ESLint disable - Use
typenotinterfacefor type definitions - Server Components are default - add
'use client'only when needed - AppSettings enum must sync between Prisma schema and
schemas/appSettings.ts - Cache invalidation - use
safeRevalidateTag()after mutations
@codaco/protocol-validation- Protocol validationlucia- Authenticationnuqs- URL state managementuploadthing- File uploadses-toolkit- Modern lodash alternativeluxon- Date/time (not moment.js)