Welcome to Hackcontrol
Hackcontrol is a modern hackathon management platform that makes organizing and participating in hackathons simple and enjoyable. This guide will walk you through how to use the platform as a participant, organizer, or judge.
Getting Started as a Participant
1. Joining a Hackathon
When an organizer shares a Hackcontrol hackathon link (like hackcontrol.dev/dQ5YRq), here's how to join:
- Visit the hackathon page using the provided link
- Sign in with GitHub - Click the "Sign in with GitHub" button and authorize the app
- View the hackathon details - Read the description, rules, and submission criteria
- Check announcements - Stay updated with important information from organizers
2. Submitting Your Project
Once you're ready to submit your project:
-
Click "Submit Project" on the hackathon page
-
Fill out the submission form:
- Project Title: Give your project a catchy name
- Description: Explain what your project does and how you built it
- Project URL: Link to your GitHub repo, live demo, or presentation
- Team Members: Add your teammates (optional)
-
Submit and track progress - Your submission will appear on the hackathon page and you can see its review status
3. Following Results
- Check review status - See if judges have reviewed your project
- View announcements - Get updates from organizers about judging progress
- See final results - Winners are announced on the hackathon page
Using Hackcontrol as an Organizer
1. Creating Your First Hackathon
After signing in and getting organizer privileges:
- Go to your dashboard (
/app) and click "Create Hackathon" - Set up your event:
- Name: Your hackathon's title
- Description: Event overview and goals
- Rules: Participation guidelines and requirements
- Criteria: How projects will be judged
2. Managing Your Hackathon
Invite Judges:
- Use the "Judge Manager" to add judges by searching for their usernames
- Judges get access to review and score submissions
Make Announcements:
- Keep participants informed with updates
- Mark important announcements to highlight them
- Announcements appear on the hackathon page in real-time
Monitor Submissions:
- View all project submissions in one place
- Track which projects have been reviewed
- Mark winners once judging is complete
Share Your Event:
- Copy and share your hackathon's URL
- Participants can join directly without additional registration
3. Ending Your Hackathon
When judging is complete:
- Review final scores - Check the leaderboard and judge feedback
- Select winners - Mark winning projects
- Finish the hackathon - This locks submissions and finalizes results
- Celebrate - Winners are displayed prominently on the hackathon page
Judging Projects
1. Accessing Your Judge Dashboard
When invited as a judge:
- Sign in with your GitHub account
- Visit the hackathon page - You'll see a "Judge Dashboard" option
- View projects to review - See all submissions you need to evaluate
2. Evaluating Projects
For each project:
- Review the submission - Read the description and visit the project URL
- Score the project - Give it a numeric score based on the hackathon criteria
- Submit your evaluation - Your score is recorded and averages with other judges
3. Judge Responsibilities
- Review all assigned projects fairly and thoroughly
- Use consistent criteria across all submissions
- Complete reviews in a timely manner to help organizers announce results
- Maintain objectivity when evaluating projects
Understanding User Roles
Regular User:
- Can join hackathons and submit projects
- View public hackathon information
- Receive announcements from organizers
Organizer:
- Create and manage hackathons
- Invite judges and manage participants
- Make announcements and control event settings
- Access analytics and submission data
Judge:
- Evaluate and score hackathon submissions
- Access special judge dashboard
- Provide feedback on projects
Tips for Success
For Participants
- Submit early - Don't wait until the last minute
- Read the criteria - Understand how you'll be judged
- Include a good demo - Make sure your project URL works and showcases your work
- Follow announcements - Stay updated on deadlines and changes
For Organizers
- Set clear expectations - Write detailed rules and judging criteria
- Communicate regularly - Use announcements to keep participants informed
- Plan judging carefully - Invite enough judges and give them clear guidelines
- Test everything - Make sure your hackathon setup works before sharing
For Judges
- Be fair and consistent - Use the same standards for all projects
- Visit project links - Actually try the demos and read the code
- Score thoughtfully - Take time to properly evaluate each submission
- Ask questions - Reach out to organizers if criteria are unclear
Getting Help
If you encounter issues or have questions:
- Check if there are any announcements from the organizer
- Look for contact information on the hackathon page
- For technical issues, contact the platform administrators
Quick Start Guide
This guide will get you up and running with Hackcontrol in under 10 minutes.
Prerequisites
- Node.js 18+ installed
- Git installed
- CockroachDB account (free tier available)
- GitHub account for OAuth
1. Clone and Install
git clone https://github.com/raducarabat/hackcontrol.git
cd hackcontrol
npm install
2. Environment Setup
Create a .env file in the root directory:
# Database
DATABASE_URL="your-cockroachdb-connection-string"
# NextAuth
NEXTAUTH_SECRET="your-secret-key"
NEXTAUTH_URL="http://localhost:3000"
# GitHub OAuth
GITHUB_CLIENT_ID="your-github-client-id"
GITHUB_CLIENT_SECRET="your-github-client-secret"
3. Database Setup
npx prisma migrate dev --name init
This creates the initial migration and applies it to your database.
4. Run Development Server
npm run dev
Open http://localhost:3000 in your browser.
5. First Login & Admin Setup
- Click "Sign in with GitHub"
- Authorize the application
- Important: Set your account as admin by running this SQL command in your CockroachDB console:
UPDATE users SET role = 'ADMIN' WHERE email = 'your-email@example.com';
Replace your-email@example.com with the email associated with your GitHub account.
- Refresh the page - you now have full admin privileges!
Next Steps
- Read the Local Installation guide for detailed setup
- Learn about Environment Configuration
- Explore the Project Structure
Local Installation
Complete guide for setting up Hackcontrol on your local development environment.
System Requirements
- Node.js: Version 18.0 or higher
- npm: Version 8.0 or higher (or pnpm/yarn)
- Git: Latest version
- Database: CockroachDB (cloud or local)
Step-by-Step Installation
1. Clone the Repository
git clone https://github.com/raducarabat/hackcontrol.git
cd hackcontrol
2. Install Dependencies
Choose your preferred package manager:
# Using npm
npm install
# Using pnpm (recommended for faster installs)
pnpm install
# Using yarn
yarn install
3. Environment Configuration
Copy the example environment file and configure it:
cp .env.example .env # If available, or create new .env
Edit .env with your configuration:
# CockroachDB connection string
DATABASE_URL="postgresql://username:password@host:port/database?sslmode=require"
# NextAuth configuration
NEXTAUTH_SECRET="your-generated-secret-here"
NEXTAUTH_URL="http://localhost:3000"
# GitHub OAuth Provider
GITHUB_CLIENT_ID="your-github-app-client-id"
GITHUB_CLIENT_SECRET="your-github-app-client-secret"
4. Database Migration
Apply the database schema:
npx prisma migrate dev --name init
This command will:
- Create migration files in
prisma/migrations/ - Apply the schema to your database
- Generate the Prisma client
5. Set Admin Privileges
After your first login, grant yourself admin access:
- Sign in through the application once
- Connect to your CockroachDB console
- Run this SQL command:
UPDATE users SET role = 'ADMIN' WHERE email = 'your-github-email@example.com';
6. Start Development Server
npm run dev
The application will be available at http://localhost:3000.
Verification
To verify your installation:
- Server starts: No errors in terminal
- Database connected: Check Prisma Studio with
npm run studio - Authentication works: Can sign in with GitHub
- Admin access: Can create new hackathons
Common Issues
- Database connection errors: Check your
DATABASE_URLformat - Authentication fails: Verify GitHub OAuth app configuration
- Migration errors: Ensure database exists and is accessible
- Port conflicts: Change port with
npm run dev -- -p 3001
Next Steps
- Configure your Environment Variables
- Set up your Database in detail
- Learn about the Project Architecture
Environment Configuration
Hackcontrol uses environment variables for configuration. This guide covers all required and optional environment variables.
Environment Variables
All environment variables are validated using Zod schemas in src/env/index.mjs.
Required Variables
Database
DATABASE_URL="postgresql://username:password@host:port/database?sslmode=require"
- Purpose: CockroachDB connection string
- Format: PostgreSQL connection URL with SSL
- Example:
postgresql://user:pass@cluster.cockroachdb.cloud:26257/hackcontrol?sslmode=require
NextAuth Configuration
NEXTAUTH_SECRET="your-secret-key-here"
NEXTAUTH_URL="http://localhost:3000"
- NEXTAUTH_SECRET: Random string for JWT signing (use
openssl rand -base64 32) - NEXTAUTH_URL: Your application's URL (production or development)
GitHub OAuth
GITHUB_CLIENT_ID="your-github-app-client-id"
GITHUB_CLIENT_SECRET="your-github-app-secret"
- GITHUB_CLIENT_ID: Public identifier from your GitHub OAuth app
- GITHUB_CLIENT_SECRET: Secret key from your GitHub OAuth app
Optional Variables
Node Environment
NODE_ENV="development" # or "production" or "test"
Vercel Deployment
VERCEL_URL="your-app.vercel.app" # Automatically set by Vercel
Environment Validation
The project uses Zod for runtime environment validation:
// Server-side validation
const server = z.object({
DATABASE_URL: z.string().url(),
NODE_ENV: z.enum(["development", "test", "production"]),
NEXTAUTH_SECRET: z.string().min(1),
NEXTAUTH_URL: z.string().url(),
GITHUB_CLIENT_ID: z.string().min(1),
GITHUB_CLIENT_SECRET: z.string().min(1),
});
Creating Environment Files
Development (.env)
DATABASE_URL="postgresql://user:pass@localhost:26257/hackcontrol_dev?sslmode=disable"
NEXTAUTH_SECRET="dev-secret-key-change-in-production"
NEXTAUTH_URL="http://localhost:3000"
GITHUB_CLIENT_ID="your-dev-github-client-id"
GITHUB_CLIENT_SECRET="your-dev-github-client-secret"
NODE_ENV="development"
Production (.env.production)
DATABASE_URL="postgresql://user:pass@cluster.cockroachdb.cloud:26257/hackcontrol?sslmode=require"
NEXTAUTH_SECRET="super-secure-production-secret"
NEXTAUTH_URL="https://your-domain.com"
GITHUB_CLIENT_ID="your-prod-github-client-id"
GITHUB_CLIENT_SECRET="your-prod-github-client-secret"
NODE_ENV="production"
Security Best Practices
- Never commit
.envfiles to version control - Use different OAuth apps for development and production
- Generate strong secrets using cryptographically secure methods
- Rotate secrets regularly in production
- Use environment-specific databases to avoid data conflicts
Generating Secrets
NEXTAUTH_SECRET
# Using OpenSSL
openssl rand -base64 32
# Using Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
Environment Validation Errors
Common validation errors and solutions:
- Invalid URL format: Ensure DATABASE_URL and NEXTAUTH_URL are properly formatted URLs
- Missing required variables: All variables in the
serverschema must be provided - Client-side access: Server variables cannot be accessed on the client side
Next Steps
- Set up your Database
- Configure GitHub OAuth
- Learn about Deployment Variables
Database Setup
Hackcontrol uses CockroachDB as its database, managed through Prisma ORM. This guide covers database setup and management.
CockroachDB Setup
1. Create a Free Cluster
- Visit CockroachDB Cloud
- Sign up for a free account
- Create a new cluster:
- Choose "Serverless" for free tier
- Select your preferred region
- Name your cluster (e.g., "hackcontrol-dev")
2. Create Database User
- In your cluster dashboard, go to "SQL Users"
- Create a new user:
- Username:
hackcontrol_user(or your preference) - Generate a strong password
- Save the credentials securely
- Username:
3. Get Connection String
- Click "Connect" on your cluster
- Select "General connection string"
- Copy the connection string
- Replace
<username>and<password>with your user credentials
Example connection string:
postgresql://hackcontrol_user:password@cluster-name.aws-region.cockroachlabs.cloud:26257/hackcontrol?sslmode=require
Database Schema
The application uses the following main models:
Core Models
- Hackathon: Main hackathon events
- Participation: User project submissions
- User: User accounts and profiles
- Judge: Judging assignments
- Score: Judging scores
- Announcement: Event announcements
Authentication Models
- Account: OAuth account linking
- Session: User sessions
- VerificationToken: Email verification
Prisma Migration
Initial Setup
Run the initial migration to create all tables:
npx prisma migrate dev --name init
Schema Changes
When you modify prisma/schema.prisma:
# Create and apply migration
npx prisma migrate dev --name describe-your-change
# Generate Prisma client
npx prisma generate
Useful Commands
# View database in Prisma Studio
npm run studio
# Reset database (development only)
npx prisma migrate reset
# Deploy migrations (production)
npx prisma migrate deploy
# Check migration status
npx prisma migrate status
Database Administration
Setting Admin Role
After your first login, grant admin privileges:
- Open CockroachDB console or Prisma Studio
- Navigate to the
userstable - Run this SQL command:
UPDATE users SET role = 'ADMIN' WHERE email = 'your-github-email@example.com';
Or using Prisma Studio:
- Open
npm run studio - Navigate to
Usermodel - Find your user record
- Change
rolefromUSERtoADMIN - Save changes
User Roles
The application supports three user roles:
- USER: Default role, can participate in hackathons
- ORGANIZER: Can create and manage hackathons
- ADMIN: Full system access, can manage all hackathons and users
Backup and Recovery
Export Data
# Export schema
npx prisma db pull
# Export data (using pg_dump)
pg_dump $DATABASE_URL > backup.sql
Restore Data
# Restore from backup
psql $DATABASE_URL < backup.sql
Troubleshooting
Connection Issues
- Verify connection string format
- Check firewall settings
- Ensure SSL mode is set correctly
Migration Errors
- Check for schema conflicts
- Verify database permissions
- Review migration history
Performance
- Monitor query performance in CockroachDB console
- Use Prisma query logging for development
- Consider database indexing for large datasets
Next Steps
- Learn about Prisma ORM
- Explore Database Schema
- Set up Development Workflow
Project Overview
Hackcontrol is a modern, full-stack hackathon management system built with cutting-edge web technologies.
What is Hackcontrol?
Hackcontrol is an open-source platform designed to streamline hackathon organization and participation. It provides tools for:
- Event Management: Create, configure, and manage hackathon events
- Participant Registration: Easy signup and project submission
- Judging System: Structured evaluation and scoring
- Team Management: Collaborative project development
- Administrative Tools: User management and event oversight
Architecture Overview
Full-Stack TypeScript
The entire application is built with TypeScript, ensuring type safety from database to UI:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Frontend │ │ API Layer │ │ Database │
│ (Next.js) │◄──►│ (tRPC) │◄──►│ (CockroachDB) │
│ React + TS │ │ TypeScript │ │ + Prisma │
└─────────────────┘ └─────────────────┘ └─────────────────┘
Key Architectural Decisions
Type-Safe API with tRPC
- End-to-end type safety
- Auto-generated TypeScript types
- Client-server contract validation
- Excellent developer experience
Prisma ORM
- Type-safe database operations
- Schema-first development
- Auto-generated client
- Migration management
Component-Based UI
- Reusable UI components with Radix UI
- Consistent design system
- Accessibility built-in
- Tailwind CSS for styling
Authentication Strategy
- NextAuth.js for session management
- GitHub OAuth integration
- JWT-based sessions
- Role-based access control
Core Features
For Participants
- GitHub authentication
- Project submission
- Team collaboration
- Progress tracking
- Real-time updates
For Organizers
- Hackathon creation and management
- Participant oversight
- Judge assignment
- Announcement system
- Event configuration
For Judges
- Streamlined evaluation interface
- Scoring system
- Project review tools
- Collaborative judging
For Administrators
- User management
- System-wide settings
- Analytics and reporting
- Platform maintenance
Technology Benefits
Developer Experience
- Hot reloading for rapid development
- Type safety prevents runtime errors
- Auto-completion throughout the stack
- Unified codebase with TypeScript
Performance
- Static generation where possible
- Incremental static regeneration
- Optimized database queries with Prisma
- Client-side caching with React Query
Scalability
- Serverless-ready architecture
- Database connection pooling
- Horizontal scaling capabilities
- CDN-optimized assets
Security
- Environment variable validation
- SQL injection prevention via Prisma
- CSRF protection built-in
- Secure authentication flows
Project Goals
- Simplicity: Easy to use for all stakeholders
- Reliability: Robust system for critical events
- Extensibility: Plugin-ready architecture
- Performance: Fast and responsive experience
- Accessibility: Inclusive design principles
Next Steps
- Explore the Technology Stack
- Understand the Project Structure
- Review the Database Schema
Technology Stack
Hackcontrol is built with a modern, production-ready technology stack focused on type safety, developer experience, and performance.
Frontend Technologies
Next.js 15.4.6
- Role: React framework and application foundation
- Features: SSR, SSG, API routes, file-based routing
- Benefits: Performance optimization, SEO, developer experience
- Configuration:
next.config.mjs
React 18.2.0
- Role: UI library for component-based architecture
- Features: Hooks, context, concurrent rendering
- Benefits: Component reusability, virtual DOM, large ecosystem
TypeScript 5.1.6
- Role: Static type checking and enhanced developer experience
- Features: Type inference, interfaces, generics
- Benefits: Catch errors at compile time, better IDE support
- Configuration:
tsconfig.json
Styling & UI
Tailwind CSS 3.3.3
- Role: Utility-first CSS framework
- Features: Responsive design, custom themes, JIT compilation
- Benefits: Rapid development, consistent design, small bundle size
- Configuration:
tailwind.config.cjs
Radix UI Primitives
- Components: Dialog, Tabs, Tooltip
- Role: Unstyled, accessible UI components
- Benefits: Accessibility compliance, keyboard navigation, ARIA support
clsx 2.0.0
- Role: Conditional CSS class utility
- Usage: Dynamic styling based on state/props
- Benefits: Clean conditional styling patterns
Framer Motion 10.15.0
- Role: Animation library for React
- Features: Declarative animations, gesture handling
- Usage: Page transitions, micro-interactions
- Location:
src/animations/
Backend & API
tRPC 10.36.0
- Role: Type-safe API layer
- Features: End-to-end type safety, auto-generated clients
- Benefits: No code generation, excellent DX, runtime safety
- Structure:
- Router definitions in
src/trpc/routers/ - Client setup in
src/trpc/api.ts
- Router definitions in
Prisma 5.1.0
- Role: Database ORM and query builder
- Features: Type-safe queries, migrations, schema management
- Benefits: Auto-generated client, great TypeScript integration
- Schema:
prisma/schema.prisma
NextAuth.js 4.24.11
- Role: Authentication solution
- Features: Multiple providers, session management, security
- Provider: GitHub OAuth
- Configuration:
src/lib/auth.ts
Database
CockroachDB
- Role: Distributed SQL database
- Features: Horizontal scaling, ACID compliance, PostgreSQL compatibility
- Benefits: High availability, strong consistency, cloud-native
- Connection: Via Prisma with PostgreSQL protocol
Data & Validation
Zod 3.25.76
- Role: Schema validation library
- Usage:
- API input validation
- Environment variable validation
- Form validation
- Location:
src/schema/andsrc/env/
Superjson 1.13.1
- Role: JSON serialization with additional types
- Features: Date, undefined, BigInt, Map, Set support
- Usage: tRPC data transformation
- Benefits: Preserves complex JavaScript types over network
React Hook Form 7.45.2
- Role: Form state management
- Features: Minimal re-renders, easy validation integration
- Benefits: Performance, DX, Zod integration
Development Tools
ESLint 8.46.0
- Role: Code linting and style enforcement
- Configuration:
.eslintrc.json - Extends: Next.js recommended config
Prettier 3.0.0
- Role: Code formatting
- Features: Opinionated formatting, Tailwind class sorting
- Plugins:
prettier-plugin-tailwindcss - Configuration:
prettier.config.cjs
TypeScript ESNext
- Target: ES5 for broad compatibility
- Module: ESNext for optimal bundling
- Features: Strict mode, path mapping (
@/*)
Additional Libraries
UI Enhancement
- Sonner 0.6.2: Toast notifications
- Canvas Confetti 1.6.0: Celebration animations
- @uiball/loaders 1.3.0: Loading indicators
- Iconoir: SVG icon library
Utilities
- nanoid 5.1.5: Unique ID generation
- next-seo 6.1.0: SEO optimization
- nextjs-progressbar 0.0.16: Page loading indicator
Package Manager
The project supports multiple package managers:
- npm (default)
- pnpm (recommended for speed)
- yarn (classic)
All package managers work with the same package.json configuration.
Build Pipeline
Development
npm run dev # Start dev server
npm run lint # Run ESLint
npm run ts:check # TypeScript compilation check
npm run format # Format code with Prettier
Production
npm run build # Build for production
npm run start # Start production server
Database
npx prisma migrate dev # Apply schema changes
npx prisma generate # Generate Prisma client
npm run studio # Open Prisma Studio
Architecture Benefits
- Type Safety: Full-stack TypeScript eliminates entire classes of runtime errors
- Developer Experience: Hot reloading, auto-completion, and excellent tooling
- Performance: Optimized builds, code splitting, and efficient rendering
- Maintainability: Clear separation of concerns and modular architecture
- Scalability: Serverless-ready components and efficient database layer
Next Steps
- Explore Project Structure
- Learn about Database Schema
- Dive into tRPC Implementation
Project Structure
Understanding the project structure is essential for effective development and maintenance.
Root Directory
hackcontrol/
├── prisma/ # Database schema and migrations
├── public/ # Static assets (images, icons)
├── src/ # Application source code
├── scripts/ # Build and utility scripts
├── .env # Environment variables (local)
├── .env.example # Environment template
├── package.json # Dependencies and scripts
├── next.config.mjs # Next.js configuration
├── tailwind.config.cjs # Tailwind CSS configuration
├── tsconfig.json # TypeScript configuration
└── vercel.json # Vercel deployment settings
Source Directory (src/)
Application Structure
src/
├── animations/ # Framer Motion animations
├── components/ # Reusable React components
├── env/ # Environment validation
├── layout/ # App layout components
├── lib/ # Utility libraries and configurations
├── pages/ # Next.js pages and API routes
├── schema/ # Zod validation schemas
├── styles/ # Global CSS and Tailwind styles
├── trpc/ # tRPC API setup and routers
├── types/ # TypeScript type definitions
└── ui/ # UI component library
Detailed Breakdown
/prisma
prisma/
├── migrations/ # Auto-generated migration files
└── schema.prisma # Database schema definition
Purpose: Database schema management and migrations
- schema.prisma: Defines all models, relationships, and database configuration
- migrations/: Version-controlled database changes
/src/animations
animations/
└── show.tsx # Page transition animations
Purpose: Framer Motion animation components
- Page transitions and micro-interactions
- Consistent animation patterns across the app
/src/components
components/
├── announcementDisplay.tsx # Announcement rendering
├── announcementManager.tsx # Admin announcement tools
├── card.tsx # Generic card component
├── copy.tsx # Copy-to-clipboard utility
├── createNew.tsx # New hackathon creation
├── deleteHackathon.tsx # Hackathon deletion
├── editHackathon.tsx # Hackathon editing
├── finishHackathon.tsx # Event completion
├── hackathonCard.tsx # Hackathon display card
├── hackathonInfo.tsx # Hackathon details
├── judgeManager.tsx # Judge management
├── judgesDashboard.tsx # Judge interface
├── leaderboardDisplay.tsx # Results display
├── loading.tsx # Loading states
├── participationCard.tsx # Participation display
├── scoringInterface.tsx # Scoring tools
├── sendProject.tsx # Project submission
├── userSearch.tsx # User lookup
└── viewProject.tsx # Project viewing
Purpose: Feature-specific React components
- Business logic components
- Form handling and data submission
- Admin and user interfaces
/src/env
env/
└── index.mjs # Environment variable validation
Purpose: Runtime environment validation using Zod
- Validates all required environment variables
- Type-safe environment access
- Development vs production configurations
/src/layout
layout/
├── header.tsx # Application header
└── footer.tsx # Application footer (if exists)
Purpose: Layout components used across all pages
- Navigation and branding
- User authentication state
- Consistent UI structure
/src/lib
lib/
├── auth.ts # NextAuth configuration
└── prisma.ts # Prisma client setup
Purpose: Core library configurations and utilities
- auth.ts: NextAuth providers, callbacks, and session handling
- prisma.ts: Database client initialization and global instance
/src/pages
pages/
├── api/
│ ├── auth/
│ │ └── [...nextauth].ts # NextAuth API route
│ └── trpc/
│ └── [trpc].ts # tRPC API handler
├── app/
│ ├── index.tsx # Dashboard/app home
│ └── [url].tsx # Individual hackathon pages
├── auth/
│ └── index.tsx # Authentication page
├── send/
│ └── [url].tsx # Project submission page
├── _app.tsx # App wrapper with providers
├── _document.tsx # HTML document structure
└── index.tsx # Landing page
Purpose: Next.js file-based routing and API endpoints
- pages/api/: Server-side API routes
- pages/app/: Main application pages
- pages/auth/: Authentication flow
- pages/send/: Project submission
/src/schema
schema/
├── announcement.ts # Announcement validation schemas
├── hackathon.ts # Hackathon validation schemas
├── participation.ts # Participation validation schemas
└── scoring.ts # Scoring validation schemas
Purpose: Zod validation schemas for API inputs
- Type-safe form validation
- API request/response validation
- Shared validation logic
/src/trpc
trpc/
├── routers/
│ ├── announcement.router.ts # Announcement API
│ ├── hackathon.router.ts # Hackathon API
│ ├── judge.router.ts # Judge API
│ ├── participation.router.ts # Participation API
│ └── scoring.router.ts # Scoring API
├── api.ts # Client-side tRPC setup
├── index.ts # tRPC initialization
└── root.ts # Main router composition
Purpose: tRPC API layer with type-safe endpoints
- routers/: Feature-specific API endpoints
- api.ts: Client configuration with React Query
- root.ts: Router composition and type exports
/src/types
types/
├── hackathon.ts # Hackathon type definitions
├── participation.ts # Participation type definitions
└── next-auth.d.ts # NextAuth type extensions
Purpose: TypeScript type definitions and extensions
- Database model types
- API response types
- Third-party library extensions
/src/ui
ui/
├── icons/ # SVG icon components
└── [components].tsx # Reusable UI components
Purpose: Design system and UI component library
- Built with Radix UI primitives
- Styled with Tailwind CSS
- Consistent design patterns
File Naming Conventions
Components
- PascalCase for component files:
HackathonCard.tsx - camelCase for utility functions:
getUserSession.ts - kebab-case for pages:
[hackathon-url].tsx
Directories
- camelCase for source directories:
src/components/ - kebab-case for page directories:
pages/api/auth/
Import Patterns
Path Aliases
// Configured in tsconfig.json
{
"paths": {
"@/*": ["./src/*"]
}
}
Usage Examples
// Absolute imports using alias
import { api } from "@/trpc/api";
import { authOptions } from "@/lib/auth";
import { HackathonCard } from "@/components/hackathonCard";
// Relative imports for nearby files
import { hackathonSchema } from "../schema/hackathon";
Configuration Files
Core Configuration
next.config.mjs: Next.js framework settingstailwind.config.cjs: Tailwind CSS customizationtsconfig.json: TypeScript compiler optionsprisma/schema.prisma: Database schema
Development Tools
.eslintrc.json: Linting rulesprettier.config.cjs: Code formatting.prettierignore: Files to skip formatting
Deployment
vercel.json: Vercel deployment configuration.env.example: Environment variable template
Development Workflow
Adding New Features
- Define types in
/src/types - Create Zod schemas in
/src/schema - Build tRPC routers in
/src/trpc/routers - Develop UI components in
/src/components - Create pages in
/src/pages
Database Changes
- Modify
prisma/schema.prisma - Run
npx prisma migrate dev --name feature-name - Update related types and schemas
- Test with
npm run studio
Next Steps
- Learn about Database Schema
- Explore tRPC API Structure
- Understand Component Development
Next.js Framework
Next.js serves as the foundation of Hackcontrol, providing the React framework with powerful features for production applications.
Next.js in Hackcontrol
Version & Configuration
- Version: Next.js 15.4.6
- Router: Pages Router (not App Router)
- Configuration:
next.config.mjs
Key Features Used
File-Based Routing
src/pages/
├── index.tsx # / (Landing page)
├── auth/
│ └── index.tsx # /auth (Sign in page)
├── app/
│ ├── index.tsx # /app (Dashboard)
│ └── [url].tsx # /app/[hackathon-url]
└── send/
└── [url].tsx # /send/[hackathon-url]
API Routes
src/pages/api/
├── auth/
│ └── [...nextauth].ts # /api/auth/* (NextAuth)
└── trpc/
└── [trpc].ts # /api/trpc/* (tRPC handler)
Configuration
Next.js Config
// next.config.mjs
!process.env.SKIP_ENV_VALIDATION && (await import("./src/env/index.mjs"));
const nextConfig = {
reactStrictMode: true,
images: {
remotePatterns: [
{
protocol: "https",
hostname: "avatars.githubusercontent.com",
port: "",
pathname: "/u/**",
},
],
},
};
export default nextConfig;
Key settings:
- Environment validation: Loads and validates env vars at build time
- React Strict Mode: Enabled for development safety
- Image optimization: Configured for GitHub avatars
TypeScript Integration
// tsconfig.json
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"strict": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
App Structure
Root App Component
// src/pages/_app.tsx
const App: AppType<{ session: Session | null }> = ({
Component,
pageProps: { session, ...pageProps },
router,
}) => {
return (
<SessionProvider session={session}>
<DefaultSeo {...nextSeoConfig} />
<NextNProgress />
<main className="font-sans">
<Header />
<Show routerKey={router.route}>
<Component {...pageProps} />
</Show>
<Toaster />
</main>
</SessionProvider>
);
};
export default api.withTRPC(App);
Features integrated:
- SessionProvider: NextAuth session context
- DefaultSeo: SEO configuration
- NextNProgress: Page loading indicator
- Show: Page transition animations
- Toaster: Toast notifications (Sonner)
- api.withTRPC: tRPC client wrapper
Document Structure
// src/pages/_document.tsx
// Custom document for HTML structure and font loading
Page Examples
Dynamic Routes
// src/pages/app/[url].tsx
import { type GetServerSideProps } from "next";
import { getServerAuthSession } from "@/lib/auth";
export default function HackathonPage() {
const router = useRouter();
const hackathonUrl = router.query.url as string;
const { data: hackathon, isLoading } = api.hackathon.getByUrl.useQuery({
url: hackathonUrl,
});
if (isLoading) return <Loading />;
if (!hackathon) return <NotFound />;
return <HackathonDetails hackathon={hackathon} />;
}
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const session = await getServerAuthSession(ctx);
return {
props: {
session,
},
};
};
API Route Handler
// src/pages/api/trpc/[trpc].ts
import { createNextApiHandler } from "@trpc/server/adapters/next";
import { appRouter } from "@/trpc/root";
import { createTRPCContext } from "@/trpc";
export default createNextApiHandler({
router: appRouter,
createContext: createTRPCContext,
onError: env.NODE_ENV === "development"
? ({ path, error }) => {
console.error(`❌ tRPC failed on ${path}: ${error.message}`);
}
: undefined,
});
Features & Integrations
Server-Side Rendering (SSR)
// For pages that need server-side data
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const session = await getServerAuthSession(ctx);
// Pre-fetch data on server
const helpers = createServerSideHelpers({
router: appRouter,
ctx: { session, prisma },
transformer: superjson,
});
await helpers.hackathon.getByUrl.prefetch({ url: ctx.params?.url as string });
return {
props: {
trpcState: helpers.dehydrate(),
session,
},
};
};
Image Optimization
// GitHub avatar optimization
<Image
src={user.image}
alt={user.name}
width={40}
height={40}
className="rounded-full"
/>
SEO Configuration
// next-seo.config.ts
export const nextSeoConfig: DefaultSeoProps = {
title: "Hackcontrol",
description: "Open-source hackathon management system",
openGraph: {
type: "website",
locale: "en_US",
url: "https://hackcontrol.vercel.app/",
siteName: "Hackcontrol",
},
};
Performance Features
Automatic Code Splitting
- Each page is automatically split into separate bundles
- Reduces initial bundle size
- Improves load times
Static Optimization
- Static pages are pre-rendered at build time
- Dynamic pages use SSR when needed
- Optimal performance for different content types
Image Optimization
- Automatic image optimization and resizing
- WebP format support
- Lazy loading by default
Development Features
Hot Reloading
- Instant feedback during development
- Preserves component state when possible
- Fast refresh for React components
Error Overlay
- Detailed error information in development
- Source map support for debugging
- Stack trace with exact file locations
TypeScript Integration
- Zero-config TypeScript support
- Fast compilation with SWC
- Built-in type checking
Build Process
Development Build
npm run dev
- Fast refresh enabled
- Source maps for debugging
- Detailed error reporting
Production Build
npm run build
npm run start
- Optimized bundles
- Static asset optimization
- Performance optimizations
Environment Handling
Environment Variables
// Automatic environment validation
!process.env.SKIP_ENV_VALIDATION && (await import("./src/env/index.mjs"));
Runtime Configuration
- Client vs server environment separation
- Secure environment variable access
- Build-time validation
Deployment
Vercel Integration
// vercel.json
{
"functions": {
"src/pages/api/**/*.ts": {
"maxDuration": 30
}
}
}
Static Assets
- Automatic optimization
- CDN distribution
- Caching headers
Best Practices
Page Organization
- Group related pages in directories
- Use meaningful file names
- Implement proper error boundaries
Performance
- Minimize client-side JavaScript
- Use dynamic imports for large components
- Optimize images and assets
SEO
- Implement proper meta tags
- Use semantic HTML structure
- Optimize for Core Web Vitals
Routing Patterns
Dynamic Routes
// [url].tsx - Single dynamic segment
// [...slug].tsx - Catch-all routes
// [[...slug]].tsx - Optional catch-all
Programmatic Navigation
import { useRouter } from "next/router";
const router = useRouter();
// Navigate to different pages
router.push("/app");
router.push(`/app/${hackathon.url}`);
router.replace("/auth"); // Replace instead of push
Next Steps
- Learn about TypeScript Configuration
- Explore Tailwind CSS Integration
- Understand Development Workflow
tRPC API Layer
tRPC provides end-to-end type safety for Hackcontrol's API layer, eliminating the need for manual API type definitions.
What is tRPC?
tRPC is a library that allows you to build fully type-safe APIs without schemas or code generation. It leverages TypeScript's type system to provide:
- End-to-end type safety: From database to UI
- Auto-completion: IDE support for API calls
- Runtime safety: Type validation at runtime
- Excellent DX: No manual type synchronization
API Architecture
Frontend → API Flow
- Frontend calls tRPC:
api.hackathon.getAll.useQuery() - tRPC routes to Next.js: All calls go to
/api/trpc/[trpc].ts - Single handler routes internally: The
[trpc].tsfile routes to the correct tRPC router - Router executes: The specific router (e.g.,
hackathon.router.ts) handles the request
Frontend Component
↓
api.hackathon.getAll.useQuery()
↓
/api/trpc/[trpc].ts (Next.js API route)
↓
hackathon.router.ts (tRPC router)
↓
Prisma → CockroachDB
Router Structure
// src/trpc/root.ts
export const appRouter = createTRPCRouter({
hackathon: hackathonRouter, // Hackathon management
participation: participationRouter, // Project submissions
announcement: announcementRouter, // Event announcements
judge: judgeRouter, // Judge management
scoring: scoringRouter, // Scoring system
});
Client Configuration
tRPC Client Setup
// src/trpc/api.ts
export const api = createTRPCNext<AppRouter>({
config() {
return {
transformer: superjson, // Handle Date, undefined, etc.
links: [
loggerLink({
enabled: (opts) =>
process.env.NODE_ENV === "development" ||
(opts.direction === "down" && opts.result instanceof Error),
}),
httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
}),
],
};
},
ssr: false, // Client-side rendering
});
App Integration
// src/pages/_app.tsx
const App: AppType<{ session: Session | null }> = ({ Component, pageProps }) => {
return (
<SessionProvider session={pageProps.session}>
<Component {...pageProps} />
</SessionProvider>
);
};
export default api.withTRPC(App); // Wrap app with tRPC
Router Examples
Public Procedures
// Anyone can call these
export const hackathonRouter = createTRPCRouter({
getAll: publicProcedure
.query(async ({ ctx }) => {
return ctx.prisma.hackathon.findMany({
include: {
participations: true,
owner: true,
},
});
}),
getByUrl: publicProcedure
.input(z.object({ url: z.string() }))
.query(async ({ ctx, input }) => {
return ctx.prisma.hackathon.findUnique({
where: { url: input.url },
});
}),
});
Protected Procedures
// Requires authentication
create: protectedProcedure
.input(hackathonSchema)
.mutation(async ({ ctx, input }) => {
return ctx.prisma.hackathon.create({
data: {
...input,
creatorId: ctx.session.user.id,
},
});
}),
delete: protectedProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
// Check ownership or admin role
const hackathon = await ctx.prisma.hackathon.findUnique({
where: { id: input.id },
});
if (hackathon?.creatorId !== ctx.session.user.id &&
ctx.session.user.role !== "ADMIN") {
throw new TRPCError({ code: "FORBIDDEN" });
}
return ctx.prisma.hackathon.delete({
where: { id: input.id },
});
}),
Usage in Components
Queries (Data Fetching)
// Simple query
const { data: hackathons, isLoading, error } = api.hackathon.getAll.useQuery();
// Query with parameters
const { data: hackathon } = api.hackathon.getByUrl.useQuery({
url: router.query.url as string
});
// Conditional queries
const { data: scores } = api.scoring.getScores.useQuery(
{ participationId: participation.id },
{ enabled: !!participation.id } // Only run if ID exists
);
Mutations (Data Changes)
const utils = api.useContext();
const createHackathon = api.hackathon.create.useMutation({
onSuccess: (newHackathon) => {
// Invalidate and refetch
utils.hackathon.getAll.invalidate();
// Or optimistic update
utils.hackathon.getAll.setData(undefined, (old) =>
old ? [...old, newHackathon] : [newHackathon]
);
toast.success("Hackathon created!");
router.push(`/app/${newHackathon.url}`);
},
onError: (error) => {
toast.error(error.message);
},
});
// Usage in form handler
const handleSubmit = (data: HackathonInput) => {
createHackathon.mutate(data);
};
Authentication Context
Context Creation
// src/trpc/index.ts
export const createTRPCContext = async (opts: CreateNextContextOptions) => {
const { req, res } = opts;
// Get session from NextAuth
const session = await getServerAuthSession({ req, res });
return {
session,
prisma,
};
};
Using Session in Procedures
// Access current user
const currentUser = ctx.session?.user;
// Check authentication
if (!ctx.session) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
// Check admin role
if (ctx.session.user.role !== "ADMIN") {
throw new TRPCError({ code: "FORBIDDEN" });
}
Data Validation
Input Schemas
// Using Zod schemas from src/schema/
import { hackathonSchema } from "@/schema/hackathon";
create: protectedProcedure
.input(hackathonSchema) // Validates input automatically
.mutation(async ({ ctx, input }) => {
// input is fully typed and validated
return ctx.prisma.hackathon.create({ data: input });
}),
Custom Validation
getParticipations: protectedProcedure
.input(z.object({
hackathonId: z.string().cuid(),
limit: z.number().min(1).max(100).default(10),
cursor: z.string().optional(),
}))
.query(async ({ ctx, input }) => {
// Pagination example
return ctx.prisma.participation.findMany({
where: { hackathonId: input.hackathonId },
take: input.limit + 1,
cursor: input.cursor ? { id: input.cursor } : undefined,
});
}),
Error Handling
Server Errors
// Standard tRPC error codes
throw new TRPCError({ code: "NOT_FOUND" }); // 404
throw new TRPCError({ code: "UNAUTHORIZED" }); // 401
throw new TRPCError({ code: "FORBIDDEN" }); // 403
throw new TRPCError({ code: "BAD_REQUEST" }); // 400
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); // 500
// With custom messages
throw new TRPCError({
code: "NOT_FOUND",
message: "Hackathon not found or access denied",
});
Client Error Handling
const { data, error, isError, isLoading } = api.hackathon.getById.useQuery({ id });
if (isError) {
// Handle specific error types
if (error.data?.code === "NOT_FOUND") {
return <NotFoundPage />;
}
if (error.data?.code === "UNAUTHORIZED") {
router.push("/auth");
return null;
}
return <ErrorDisplay message={error.message} />;
}
Performance Optimization
Request Batching
tRPC automatically batches multiple requests:
// These will be batched into a single HTTP request
const hackathons = api.hackathon.getAll.useQuery();
const announcements = api.announcement.getAll.useQuery();
const userProfile = api.user.getProfile.useQuery();
Caching Strategies
// Cache indefinitely for static data
const { data } = api.hackathon.getById.useQuery(
{ id },
{
staleTime: Infinity,
cacheTime: Infinity,
}
);
// Background refetch for dynamic data
const { data } = api.participation.getAll.useQuery(
undefined,
{
refetchInterval: 30000, // Refetch every 30 seconds
refetchOnWindowFocus: true,
}
);
Type Inference
Router Types
// Auto-generated type helpers
import type { RouterInputs, RouterOutputs } from "@/trpc/api";
// Input types
type HackathonCreateInput = RouterInputs["hackathon"]["create"];
type ParticipationUpdateInput = RouterInputs["participation"]["update"];
// Output types
type HackathonData = RouterOutputs["hackathon"]["getById"];
type ParticipationList = RouterOutputs["participation"]["getAll"];
Best Practices
Router Organization
- One router per feature domain
- Keep procedures focused and single-purpose
- Use consistent naming conventions
Input Validation
- Always validate inputs with Zod
- Reuse schemas between routers
- Provide meaningful error messages
Authentication
- Use middleware for common auth logic
- Implement role-based access control
- Handle unauthorized access gracefully
Error Handling
- Use appropriate tRPC error codes
- Provide user-friendly messages
- Log errors for debugging
Next Steps
- Learn about Prisma Integration
- Explore Zod Validation
- Understand API Development Workflow
Prisma ORM
Prisma serves as the database layer for Hackcontrol, providing type-safe database operations and schema management.
What is Prisma?
Prisma is a next-generation ORM that provides:
- Type-safe database access
- Auto-generated client
- Schema-first development
- Migration management
- Query optimization
Database Configuration
Schema Definition
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "cockroachdb"
url = env("DATABASE_URL")
}
Client Setup
// src/lib/prisma.ts
import { PrismaClient } from "@prisma/client";
import { env } from "@/env/index.mjs";
export const prisma =
global.prisma ||
new PrismaClient({
log: env.NODE_ENV === "development"
? ["query", "error", "warn"]
: ["error"],
});
if (env.NODE_ENV !== "production") {
global.prisma = prisma;
}
Core Models
Hackathon Model
model Hackathon {
id String @id @default(cuid())
name String
url String @unique
description String?
rules String?
criteria String?
owner User? @relation(fields: [creatorId], references: [id])
creatorId String
updatedAt DateTime @default(now()) @updatedAt
is_finished Boolean @default(false)
verified Boolean @default(false)
min_judges_required Int @default(2)
participations Participation[]
announcements Announcement[]
Judge Judge[]
@@unique([url, creatorId])
@@map(name: "hackathons")
}
User Model
model User {
id String @id @default(cuid())
name String?
username String? @unique
email String? @unique
emailVerified DateTime?
image String?
role Role @default(USER)
access Boolean @default(true)
accounts Account[]
sessions Session[]
hackathons Hackathon[]
participations Participation[]
judgedHackathons Judge[]
invitedJudges Judge[] @relation("JudgeInviter")
@@map(name: "users")
}
Participation Model
model Participation {
id String @id @default(cuid())
is_reviewed Boolean @default(false)
is_winner Boolean @default(false)
title String
description String
project_url String
hackathon_name String
hackathon_url String
createdBy User? @relation(fields: [creatorId], references: [id])
creatorId String
creatorName String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
hackathon Hackathon? @relation(fields: [hackathon_url], references: [url], onDelete: Cascade)
scores Score[]
team_members Json?
@@unique([creatorId, hackathon_url])
@@map(name: "participations")
}
Usage in tRPC Routers
Basic Queries
// src/trpc/routers/hackathon.router.ts
export const hackathonRouter = createTRPCRouter({
getAll: publicProcedure
.query(async ({ ctx }) => {
return ctx.prisma.hackathon.findMany({
include: {
participations: {
include: {
createdBy: true,
},
},
owner: true,
},
orderBy: {
updatedAt: "desc",
},
});
}),
getByUrl: publicProcedure
.input(z.object({ url: z.string() }))
.query(async ({ ctx, input }) => {
const hackathon = await ctx.prisma.hackathon.findUnique({
where: { url: input.url },
include: {
participations: {
include: {
createdBy: true,
scores: true,
},
},
announcements: {
orderBy: { createdAt: "desc" },
},
Judge: {
include: {
user: true,
},
},
},
});
if (!hackathon) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Hackathon not found",
});
}
return hackathon;
}),
});
Mutations with Validation
create: protectedProcedure
.input(hackathonSchema)
.mutation(async ({ ctx, input }) => {
// Check if URL is unique
const existing = await ctx.prisma.hackathon.findUnique({
where: { url: input.url },
});
if (existing) {
throw new TRPCError({
code: "CONFLICT",
message: "Hackathon URL already exists",
});
}
return ctx.prisma.hackathon.create({
data: {
...input,
creatorId: ctx.session.user.id,
},
include: {
owner: true,
},
});
}),
Advanced Prisma Features
Transactions
// Atomic operations
updateWithScores: protectedProcedure
.input(participationUpdateSchema)
.mutation(async ({ ctx, input }) => {
return ctx.prisma.$transaction(async (tx) => {
// Update participation
const participation = await tx.participation.update({
where: { id: input.id },
data: input.data,
});
// Update related scores
await tx.score.updateMany({
where: { participationId: input.id },
data: { updatedAt: new Date() },
});
return participation;
});
}),
Aggregations
getStats: publicProcedure
.input(z.object({ hackathonId: z.string() }))
.query(async ({ ctx, input }) => {
const stats = await ctx.prisma.participation.aggregate({
where: { hackathon: { id: input.hackathonId } },
_count: { id: true },
_avg: { scores: { score: true } },
});
return {
totalParticipations: stats._count.id,
averageScore: stats._avg.scores?.score || 0,
};
}),
Raw Queries
// For complex queries not easily expressed in Prisma
getLeaderboard: publicProcedure
.input(z.object({ hackathonUrl: z.string() }))
.query(async ({ ctx, input }) => {
return ctx.prisma.$queryRaw`
SELECT
p.id,
p.title,
p."creatorName",
AVG(s.score)::float as average_score,
COUNT(s.id) as judge_count
FROM participations p
LEFT JOIN scores s ON p.id = s."participationId"
WHERE p.hackathon_url = ${input.hackathonUrl}
GROUP BY p.id, p.title, p."creatorName"
ORDER BY average_score DESC NULLS LAST
`;
}),
Migration Management
Development Workflow
# Modify prisma/schema.prisma
# Then create migration
npx prisma migrate dev --name add-team-system
# This will:
# 1. Generate migration SQL
# 2. Apply to database
# 3. Regenerate Prisma client
Production Deployment
# Deploy migrations to production
npx prisma migrate deploy
# Generate client (usually in build process)
npx prisma generate
Schema Changes
When adding new fields:
model Participation {
// ... existing fields
team_members Json? // New field
submission_url String? // Another new field
}
Then run:
npx prisma migrate dev --name add-team-features
Database Tools
Prisma Studio
npm run studio
Visual database browser for:
- Viewing and editing data
- Testing queries
- Managing relationships
- Debugging data issues
Query Logging
Enable in development:
const prisma = new PrismaClient({
log: ["query", "error", "warn"], // See all SQL queries
});
Best Practices
Model Design
- Use descriptive field names
- Add appropriate indexes for performance
- Define proper relationships
- Use enums for constrained values
Query Optimization
- Include only needed relations
- Use select for specific fields
- Implement pagination for large datasets
- Consider database indexes
Error Handling
- Check for null results
- Handle unique constraint violations
- Provide meaningful error messages
- Use transactions for complex operations
Security
- Validate all inputs
- Check user permissions
- Sanitize user data
- Use parameterized queries (automatic with Prisma)
Common Patterns
Finding with Relations
const hackathonWithData = await prisma.hackathon.findUnique({
where: { url: hackathonUrl },
include: {
participations: {
include: {
createdBy: true,
scores: {
include: {
judge: {
include: {
user: true,
},
},
},
},
},
},
announcements: true,
},
});
Creating with Relations
const participation = await prisma.participation.create({
data: {
title: input.title,
description: input.description,
project_url: input.projectUrl,
hackathon: {
connect: { url: input.hackathonUrl },
},
createdBy: {
connect: { id: ctx.session.user.id },
},
},
include: {
hackathon: true,
createdBy: true,
},
});
Next Steps
- Learn about Zod Validation
- Explore Database Schema
- Understand Development Workflow
NextAuth.js Authentication
NextAuth.js provides secure, flexible authentication for Hackcontrol with GitHub OAuth integration.
Authentication Setup
Configuration
// src/lib/auth.ts
export const authOptions: NextAuthOptions = {
// Session strategy
session: {
strategy: "jwt",
},
// Database adapter
adapter: PrismaAdapter(prisma),
// OAuth providers
providers: [
GithubProvider({
clientId: env.GITHUB_CLIENT_ID,
clientSecret: env.GITHUB_CLIENT_SECRET,
profile(profile) {
return {
id: profile.id.toString(),
name: profile.name || profile.login,
username: profile.login,
email: profile.email,
image: profile.avatar_url,
};
},
}),
],
// Custom pages
pages: {
signIn: "/auth",
},
};
API Route
// src/pages/api/auth/[...nextauth].ts
import { authOptions } from "@/lib/auth";
import NextAuth from "next-auth";
export default NextAuth(authOptions);
Authentication Flow
1. User Access
User visits protected page
↓
Check session status
↓
No session? → Redirect to /auth
↓
Session exists? → Allow access
2. GitHub OAuth Flow
Click "Sign in with GitHub"
↓
Redirect to GitHub OAuth
↓
User authorizes application
↓
GitHub redirects back with code
↓
NextAuth exchanges code for user data
↓
Create/update user in database
↓
Generate JWT session token
↓
Set session cookie
Session Management
JWT Strategy
// JWT callbacks
callbacks: {
async jwt({ token, user }) {
const dbUser = await prisma.user.findFirst({
where: { email: token.email },
});
if (!dbUser) {
token.id = user?.id;
token.role = "USER";
return token;
}
return {
id: dbUser.id,
name: dbUser.name,
username: dbUser.username,
email: dbUser.email,
image: dbUser.image,
role: dbUser.role || "USER",
};
},
async session({ token, session }) {
if (token) {
session.user.id = token.id;
session.user.name = token.name;
session.user.username = token.username;
session.user.email = token.email;
session.user.image = token.image;
session.user.role = token.role;
}
return session;
},
},
Server-Side Session Access
// In API routes or getServerSideProps
export const getServerAuthSession = async (ctx: {
req: GetServerSidePropsContext["req"];
res: GetServerSidePropsContext["res"];
}) => {
return await getServerSession(ctx.req, ctx.res, authOptions);
};
// Usage in pages
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const session = await getServerAuthSession(ctx);
if (!session) {
return {
redirect: {
destination: "/auth",
permanent: false,
},
};
}
return { props: { session } };
};
Client-Side Usage
Session Provider
// src/pages/_app.tsx
import { SessionProvider } from "next-auth/react";
export default function App({ session, ...appProps }) {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
);
}
Using Sessions in Components
import { useSession, signIn, signOut } from "next-auth/react";
export default function Header() {
const { data: session, status } = useSession();
if (status === "loading") return <Loading />;
if (session) {
return (
<div>
<p>Signed in as {session.user.email}</p>
<img src={session.user.image} alt="Profile" />
<button onClick={() => signOut()}>Sign out</button>
</div>
);
}
return (
<div>
<p>Not signed in</p>
<button onClick={() => signIn("github")}>Sign in with GitHub</button>
</div>
);
}
Protected Routes
Page-Level Protection
// Higher-order component for protection
export function withAuth<P extends {}>(Component: React.ComponentType<P>) {
return function AuthenticatedComponent(props: P) {
const { data: session, status } = useSession();
const router = useRouter();
useEffect(() => {
if (status === "loading") return;
if (!session) router.push("/auth");
}, [session, status, router]);
if (status === "loading") return <Loading />;
if (!session) return null;
return <Component {...props} />;
};
}
// Usage
export default withAuth(function ProtectedPage() {
return <div>This page requires authentication</div>;
});
API Route Protection
// tRPC protected procedure
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session || !ctx.session.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: {
session: { ...ctx.session, user: ctx.session.user },
},
});
});
Role-Based Access Control
User Roles
// prisma/schema.prisma
enum Role {
ADMIN
ORGANIZER
USER
}
model User {
role Role @default(USER)
// ... other fields
}
Role Checks
// tRPC admin procedure
export const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
if (ctx.session.user.role !== "ADMIN") {
throw new TRPCError({
code: "FORBIDDEN",
message: "Admin access required"
});
}
return next({ ctx });
});
// Component role checking
function AdminPanel() {
const { data: session } = useSession();
if (session?.user.role !== "ADMIN") {
return <div>Access denied</div>;
}
return <div>Admin content</div>;
}
Custom Auth Pages
Sign In Page
// src/pages/auth/index.tsx
import { getProviders, signIn, getSession } from "next-auth/react";
import { GetServerSideProps } from "next";
export default function SignIn({ providers }) {
return (
<div>
<h1>Sign in to Hackcontrol</h1>
{Object.values(providers).map((provider) => (
<div key={provider.name}>
<button onClick={() => signIn(provider.id)}>
Sign in with {provider.name}
</button>
</div>
))}
</div>
);
}
export const getServerSideProps: GetServerSideProps = async (context) => {
const session = await getSession(context);
if (session) {
return { redirect: { destination: "/app" } };
}
const providers = await getProviders();
return { props: { providers } };
};
Database Integration
Prisma Adapter Models
// NextAuth required models
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}
GitHub OAuth Setup
1. Create GitHub OAuth App
- Go to GitHub Settings > Developer settings > OAuth Apps
- Click "New OAuth App"
- Fill in details:
- Application name: "Hackcontrol"
- Homepage URL: "http://localhost:3000" (dev) or your domain
- Authorization callback URL: "http://localhost:3000/api/auth/callback/github"
2. Environment Variables
GITHUB_CLIENT_ID="your-client-id"
GITHUB_CLIENT_SECRET="your-client-secret"
3. Production Setup
For production, update:
- Homepage URL to your domain
- Callback URL to
https://yourdomain.com/api/auth/callback/github
Security Features
CSRF Protection
- Built-in CSRF protection
- Automatic token validation
- Secure cookie handling
Session Security
// Secure session configuration
session: {
strategy: "jwt",
maxAge: 30 * 24 * 60 * 60, // 30 days
updateAge: 24 * 60 * 60, // 24 hours
},
Cookie Configuration
cookies: {
sessionToken: {
name: "next-auth.session-token",
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: process.env.NODE_ENV === "production",
},
},
},
Troubleshooting
Common Issues
-
Callback URL Mismatch
- Ensure GitHub OAuth app callback URL matches exactly
- Include protocol (http/https)
-
Environment Variables
- Verify GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET
- Check NEXTAUTH_SECRET is set
-
Database Issues
- Ensure Prisma schema includes NextAuth models
- Run database migrations
Debug Mode
NEXTAUTH_DEBUG=true # Enable detailed logging
Best Practices
Security
- Always use HTTPS in production
- Set secure environment variables
- Implement proper role-based access
- Regularly rotate secrets
User Experience
- Provide clear sign-in/out flows
- Handle loading states gracefully
- Show appropriate error messages
- Implement session persistence
Performance
- Use JWT for stateless sessions
- Cache user roles appropriately
- Minimize database queries in callbacks
Next Steps
- Learn about User Management
- Explore Role-Based Features
- Understand Security Best Practices