A conference platform for browsing, registering, and managing tech conferences.
ConNexus lets users:
- Browse and search conferences with real-time filtering
- Register for conferences with form validation
- Favorite conferences for later
- Manage their profile and registrations
- View upcoming and past events
Admins can:
- Create and edit conferences
- Manage attendee registrations
- Track conference capacity and stats
- Framework: Next.js 15.5.5 with App Router
- Database: PostgreSQL with Prisma ORM
- Authentication: NextAuth.js v4 with GitHub OAuth
- Styling: Tailwind CSS v4
- Language: TypeScript throughout
- Node.js 20+
- PostgreSQL database (or use a hosted option like Supabase/Neon)
- GitHub OAuth app for authentication
- Clone and install dependencies:
git clone git@github.com:josephbrockw/connexus.git
cd connexus
npm install- Start the local PostgreSQL database:
docker-compose up -d- Set up your environment variables in
.env.local:
# Database (matches docker-compose.yml)
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/myapp_dev"
DIRECT_DATABASE_URL="postgresql://postgres:postgres@localhost:5432/myapp_dev"
# NextAuth
NEXTAUTH_URL="http://localhost:3001"
NEXTAUTH_SECRET="your-secret-key-here"
# GitHub OAuth (create at github.com/settings/developers)
GITHUB_ID="your-github-oauth-id"
GITHUB_SECRET="your-github-oauth-secret"
# App
NEXT_PUBLIC_APP_URL="http://localhost:3001"- Set up the database:
npx prisma generate
npx prisma migrate deploy
npm run db:seed # Optional: adds sample data- Run the dev server:
npm run devOpen http://localhost:3001 to see it running.
Here are some of the key patterns I implemented and why:
I extracted all fetch calls into dedicated API clients (/src/lib/api/):
Why: Every component was doing its own error handling, JSON parsing, and header management. This was repetitive and hard to maintain.
What I built:
apiClient.ts- Base fetch wrapper with automatic error handlingconferenceApi.ts- All conference-related endpointsfavoriteApi.ts- Favorite toggle operationsadminApi.ts- Admin CRUD operationsuserApi.ts- User profile management
Benefits:
- Changed error handling once, fixed it everywhere
- Type-safe API responses
- Easy to mock for testing
- Centralized place to add auth headers or logging
Trade-off: Added about 280 lines of code to save 44 lines in components. Net increase, but the maintenance win is worth it if the codebase grows.
I created reusable hooks for common patterns (/src/hooks/):
Why: Components were duplicating state management logic, especially for pagination and debouncing.
What I built:
useDebounce- Delays updates (for search inputs)useInfiniteScroll- Generic infinite scroll pagination
Benefits:
- Infinite scroll is reusable for any future data lists
- Debouncing prevents excessive API calls
- Easier to test business logic separately from UI
Trade-off: Added about 150 lines of hook code.
I used NextAuth v4 without middleware:
Why: Middleware can be tricky with Next.js App Router, and with such a small application, it's not worth the complexity.
How it works:
- Each protected page calls
getServerSession(authOptions)server-side - If no session, redirect to
/api/auth/signin - Client components use
useSession()for conditional rendering
Benefits:
- Explicit and predictable
- No mysterious middleware redirects
- Easy to debug
Trade-off: Have to remember to add the auth check to every protected page. Middleware would be automatic but less flexible.
Filtering happens in the API route, not the client:
Why: Started with client-side filtering, but realized I'd eventually load all conferences which doesn't scale.
How it works:
- Client sends filter params to
/api/conferences/search - Server filters in the database query
- Returns paginated results using limit and offset pattern
Benefits:
- Can handle thousands of conferences
- Faster initial page load
- Reduces bandwith (only sends data for visible items)
- Consistent number of items rendered initially
I used TypeScript strictly with shared types (/src/types/):
Why: Wanted to catch bugs at compile time, not runtime.
Patterns:
- Shared interfaces for Conference, User, Registration
- API response types
- Form data types
- Validation schemas
Benefits:
- Autocomplete everywhere
- Refactoring is safer
- Bugs caught before deployment
Trade-off: More verbose code. Have to update types when schema changes.
I implemented root-level error boundaries using Next.js App Router conventions:
What I built:
error.tsx- Catches runtime errors with a "Try Again" button that resets the error boundarynot-found.tsx- Handles 404 pages with a "Go Home" link
Benefits:
- Users see branded error pages instead of white screens
- Error boundary allows recovery without full page refresh
- Both pages use the theme system (light/dark mode support)
- Development mode shows error messages for debugging
Trade-off: Only root-level boundaries. Errors crash the whole app instead of isolated sections.
I used Prisma with PostgreSQL. Main models:
- User - User accounts with GitHub OAuth
- Conference - Events with dates, pricing, capacity
- Registration - User registrations with contact info
- Favorite - Saved conferences per user
- Account/Session - NextAuth tables
Key relationships:
- User has many Registrations and Favorites
- Conference has many Registrations and Favorites
- Cascading deletes when conferences are removed
Check prisma/schema.prisma for the full schema.
src/
├── app/ # Next.js App Router pages
│ ├── api/ # API routes
│ ├── admin/ # Admin dashboard
│ ├── auth/ # Auth pages
│ └── dashboard/ # User dashboard
├── components/
│ ├── conference/ # Conference-specific components
│ ├── dashboard/ # Dashboard components
│ ├── admin/ # Admin components
│ ├── forms/ # Reusable form inputs
│ ├── ui/ # Generic UI components
│ └── providers/ # React context providers
├── lib/
│ ├── api/ # API client layer
│ ├── filterConferences.ts
│ ├── validation.ts # Form validators
│ └── admin.ts # Admin utilities
├── hooks/ # Custom React hooks
├── types/ # TypeScript definitions
├── config/ # App configuration
└── db/ # Prisma client
npm run dev # Start dev server (localhost:3001)
npm run build # Production build
npm start # Run production server
npm run lint # Run ESLint
npm run db:seed # Seed database with sample data
npm run db:seed:large # Seed database with large sample data
npx prisma studio # Open database GUI
npx prisma migrate dev # Create new migrationIf I kept working on this, here's what I'd tackle:
Better error handling:
- Toast notifications instead of
alert() - Add route-specific error boundaries (e.g.
/dashboard/error.tsx,/admin/error.tsx) to isolate failures - Retry logic for failed API calls
- Error logging service integration
Testing:
- Unit tests for API clients and hooks
- Integration tests for critical flows (registration, favorites)
- E2E tests for happy paths
Performance:
- Add data caching and background refetch
- Optimize images with proper sizing and lazy loading
- Consider virtual scrolling for long conference lists
Better validation:
- Replace custom validators with Zod
- Share validation schema between client and server
- More specific error messages
Search improvements:
- Full-text search in PostgreSQL
- Search by location/date range
- "No results" suggestions
Admin features:
- Bulk operations (delete multiple, export data)
- Conference analytics dashboard
- Email attendees functionality
Accessibility:
- Keyboard navigation audit
- Screen reader testing
- Better focus management in modals