CLAUDE.md
Project documentation and guidelines for Claude Code
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
Next.js 16 application using App Router, TypeScript, and Tailwind CSS v4. Configured for Supabase (PostgreSQL) with raw SQL queries via postgres-js, NextAuth v5 authentication with custom adapter, and React Hook Form + Zod validation.
Velkommen til prosjektdokumentasjonen. Dokumentasjonen er delt opp i flere filer for bedre oversikt.
Quick Links
Features
- Brukerprofil - Kravspesifikasjon: Brukerprofilinnstillinger
- Venner - Kravspesifikasjon: Venner (Friends feature)
- Followers - Implementasjon: Følgere (Followers feature)
- Subscriptions - Kravspesifikasjon: Abonnementer (Free/Premium)
- Language Support - Kravspesifikasjon: Flerspråklig støtte (Norwegian/English)
- Forgot Password - Kravspesifikasjon: Passordtilbakestilling (Password Reset)
API & Android Support
- API Migration Guide - Guide for å migrere Server Actions til REST API for Android-støtte
Standards
- Responsive Design - 3-breakpoint strategi for mobil, tablet og laptop
- Project Structure - Hybrid mappestruktur (app/ + src/)
- Authentication - NextAuth v5 med Google OAuth + Credentials
- Timezone - UTC lagring + lokal visning
Development Commands
npm run dev # Start dev server (http://localhost:3000)
npm run build # Build for production
npm start # Run production server
npm run lint # Run ESLint
Upgrading Next.js
To upgrade Next.js to the latest version, use the official codemod tool:
# Upgrade to latest stable version
npx @next/codemod@latest upgrade
# The codemod will:
# 1. Update Next.js and React versions in package.json
# 2. Apply necessary code transformations
# 3. Update configuration files if needed
# 4. Show migration guides for breaking changes
# After running the codemod:
npm install # Install updated dependencies
npm run build # Test that everything builds
Important Notes:
- Always commit your changes before running the upgrade
- Test thoroughly after upgrading, especially:
- Authentication flows (NextAuth)
- API routes
- Server Actions
- Image optimization
- Check the Next.js release notes for breaking changes
- Current version: Next.js 16.0.8 + React 19.2.1
Tech Stack
- Framework: Next.js 16.0.8 (App Router) + React 19.2.1
- Language: TypeScript 5 (strict mode enabled)
- Database: PostgreSQL via Supabase (direct connection with postgres-js)
- Database Client: postgres 3.4.7 (raw SQL queries, no ORM)
- Auth: NextAuth 5.0.0-beta.30 with custom SQL adapter
- Email Service: Brevo (formerly Sendinblue) @getbrevo/brevo 3.0.1 for transactional emails
- Rate Limiting: Upstash Redis @upstash/ratelimit 2.0.7 + @upstash/redis 1.35.7
- Signup: 5 requests per hour
- Friend requests: 10 requests per hour
- ID Generation: Custom NanoID implementation (PostgreSQL function) for URL-safe post IDs
- 12-character alphanumeric IDs (62^12 combinations)
- Cryptographically secure via
gen_random_bytes()
- Styling: Tailwind CSS v4 + Class Variance Authority + clsx + tailwind-merge
- Forms: React Hook Form 7.66.0 + @hookform/resolvers + Zod 4.1.12
- Icons: Lucide React 0.553.0
- Security: bcrypt 6.0.0
Key Configuration
TypeScript
TypeScript is configured with strict mode and custom path aliases.
Path Aliases tsconfig.js
{
"@/*": ["./*"], // Root directory
"@/lib/*": ["./src/lib/*"], // Utilities and configs
"@/types/*": ["./src/types/*"], // Type declarations
"@/hooks/*": ["./src/hooks/*"], // React hooks
"@/repositories": ["./src/repositories"],
"@/repositories/*": ["./src/repositories/*"]
}
Type Declarations
- Location:
src/types/ - Module augmentations: For extending third-party types (e.g., NextAuth)
- typeRoots:
["./node_modules/@types", "./src/types"]
Settings
- Target: ES2017
- Module: ESNext with bundler resolution
- Strict mode enabled
- JSX: react-jsx (React 19)
Tailwind CSS v4 + Dark Mode
Uses new @import "tailwindcss" syntax (not @tailwind directives). Theme configured inline in app/globals.css with comprehensive CSS variables for dark mode support.
Dark Mode Implementation
The application includes a fully functional dark mode with user preferences stored in the database.
Features:
- Three theme modes: Light, Dark, System (follows OS preference)
- User preference storage: Saved in PostgreSQL
users.themecolumn - Persistent state: localStorage fallback + database sync for authenticated users
- Smooth transitions: 150ms CSS transitions on color changes
- Global toggle: Available in header (desktop + mobile navigation)
Key Components:
src/contexts/ThemeContext.tsx- React Context for theme state managementcomponents/ThemeToggle.tsx- Theme toggle UI componentsapp/api/user/theme/route.ts- API endpoint to update user theme preferenceapp/globals.css- CSS variables for light/dark modes
Usage in Components:
// Use Tailwind's dark: prefix for dark mode styles
className="bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
// Access theme programmatically (client components only)
import { useTheme } from '@/src/contexts/ThemeContext'
const { theme, setTheme, resolvedTheme } = useTheme()
Color Contrast Guidelines:
- Light mode: Dark text on light backgrounds (
text-gray-900onbg-white) - Dark mode: Light text on dark backgrounds (
dark:text-gray-100ondark:bg-gray-800) - Always specify both light and dark colors for text elements
- Test visibility in both modes before deploying
Project Structure
Full documentation: docs/standards/project-structure.md
Hybrid structure: Pages in app/, shared code in src/.
project-root/
├── app/ # Next.js App Router - Pages, layouts, API routes
├── src/ # Shared code: lib/, types/, contexts/, repositories/
├── db/ # Database: schema.ts, index.ts, migrations/
├── lib/ # Root utilities (cn function)
└── components/ # Reusable UI components
Critical rules:
- API routes MUST be in
app/api/, NOTsrc/app/api/ - Auth config in
src/lib/auth.ts, NOT project root - Type extensions in
src/types/
Architecture Notes
Repository Pattern (CRITICAL)
IMPORTANT: ALL database access MUST go through repositories. NEVER import sql directly outside of repositories.
Structure
- All repositories in
src/repositories/ - Import via
import { usersRepository } from '@/repositories' - Each repository encapsulates all database operations for a single entity
- Uses raw SQL queries with postgres-js for maximum control and simplicity
Usage Example
// ✅ CORRECT - Use repository
import { usersRepository } from '@/repositories'
const user = await usersRepository.getUserByEmail(email)
await usersRepository.createUser(data)
await usersRepository.updateUser(id, updates)
await usersRepository.deleteUser(id)
// ❌ WRONG - Never import sql directly
import { sql } from '@/db' // DON'T DO THIS outside repositories
Available Repositories
-
usersRepository: User CRUD operations (raw SQL)
getUserById(id)- Fetch user by IDgetUserByEmail(email)- Fetch user by email (auto-lowercases)createUser(input)- Create new user (auto-lowercases email)updateUser(id, data)- Update user (auto-lowercases email if provided)deleteUser(id)- Delete useruserExists(email)- Check if user exists
-
friendshipsRepository: Friendship CRUD operations (raw SQL)
createFriendRequest(requesterId, addresseeId)- Send friend requestacceptFriendRequest(friendshipId, userId)- Accept friend requestrejectFriendRequest(friendshipId, userId)- Reject friend requestblockUser(requesterId, addresseeId)- Block userunblockUser(friendshipId, userId)- Unblock userremoveFriend(friendshipId, userId)- Remove friendgetFriendshipStatus(userId1, userId2)- Get friendship statusareFriends(userId1, userId2)- Check if users are friendsisBlocked(requesterId, addresseeId)- Check if blockedisBlockedBidirectional(userId1, userId2)- Check blocking both waysgetFriends(userId, options)- Get friends list with search/sortgetPendingRequests(userId)- Get incoming friend requestsgetSentRequests(userId)- Get outgoing friend requestsgetBlockedUsers(userId)- Get blocked users listgetFriendsCount(userId)- Get total friends countgetPendingRequestsCount(userId)- Get pending requests count
-
followersRepository: Followers CRUD operations (raw SQL)
followUser(followerId, followingId)- Follow a user (one-way, no approval needed)unfollowUser(followerId, followingId)- Unfollow a userisFollowing(followerId, followingId)- Check if user is following anothergetFollowing(userId, limit, offset)- Get list of users this user followsgetFollowers(userId, limit, offset)- Get list of users following this usergetFollowerCounts(userId)- Get follower/following counts (from denormalized counters)getFollowRelationship(userId1, userId2)- Get relationship ('none' | 'following' | 'followed_by' | 'mutual')removeFollower(userId, followerId)- Remove a follower
-
postsRepository: Posts CRUD with visibility checks and NanoID support (raw SQL)
getPostFeed(limit, offset)- Get public posts (first image only)getPostFeedWithAllMedia(limit, offset)- Get public posts with ALL images (for carousel)getPostById(id)- Get post by numeric ID (internal use)getPostByPublicId(publicId)- Get post by public_id (URL-safe ID)getPostWithMedia(id)- Get post with media by numeric IDgetPostWithMediaByPublicId(publicId)- Get post with media by public_idgetPostWithVisibilityCheck(postId, viewerId)- Check accessgetPostByUserIdWithVisibility(profileUserId, viewerId, limit, offset)- User's postsgetFriendsFeed(userId, limit, offset)- Friends-only feedcanViewPost(postId, viewerId)- Permission check
Benefits
- Centralized database logic with raw SQL
- No ORM overhead - direct control over queries
- Easier testing and mocking
- Consistent error handling
- Type safety with manual TypeScript interfaces
- Automatic email normalization (lowercase)
Database Schema
- Location:
db/schema.tscontains TypeScript interfaces - Types: Manually defined (User, Account, Session, VerificationToken, Friendship, Post, PostMedia)
- Mapping: Helper functions in
db/index.tsconvert snake_case to camelCase
NanoID for URL-Safe Post IDs
Posts use NanoID for public URLs instead of sequential numeric IDs to improve security and privacy.
Why NanoID?
- 🔒 Security: Prevents enumeration attacks (
/posts/1,/posts/2→ impossible to guess) - 📊 Privacy: Hides total number of posts in the system
- 🔗 Shareable: Clean, URL-safe IDs like
/posts/aBc12Xyz3456 - ✨ Professional: Same pattern as YouTube, Twitter, Instagram
Implementation Details
Database Function (db/migrations/029_add_public_id_to_posts.sql):
CREATE OR REPLACE FUNCTION generate_nanoid(size INT DEFAULT 12)
RETURNS TEXT AS $$
DECLARE
alphabet TEXT := '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
bytes BYTEA := gen_random_bytes(size);
result TEXT := '';
i INT;
BEGIN
FOR i IN 0..size-1 LOOP
result := result || substr(alphabet, get_byte(bytes, i) % 62 + 1, 1);
END LOOP;
RETURN result;
END;
$$ LANGUAGE plpgsql;
Database Column:
ALTER TABLE lysglimt
ADD COLUMN public_id VARCHAR(12) UNIQUE NOT NULL DEFAULT generate_nanoid(12);
CREATE INDEX idx_lysglimt_public_id ON lysglimt(public_id);
TypeScript Interface (db/schema.ts):
export interface Post {
id: number // Internal numeric ID (for JOINs, foreign keys)
publicId: string // Public URL-safe ID (e.g., 'aBc12Xyz3456')
userId: number
content: string | null
// ... other fields
}
Usage Patterns
✅ CORRECT - Use publicId for URLs:
// Fetch post by publicId (from URL parameter)
const post = await postsRepository.getPostWithMediaByPublicId(publicId)
// Create links with publicId
<Link href={`/posts/${post.publicId}`}>View Post</Link>
// Redirect after creating post
router.push(`/posts/${newPost.publicId}`)
⚠️ INTERNAL USE ONLY - Numeric ID:
// Use numeric ID only for database operations (JOINs, foreign keys)
const post = await postsRepository.getPostById(numericId)
// Comments, likes, etc. still use numeric post_id as foreign key
await commentsRepository.createComment({ postId: post.id, ... })
Routes
- Post detail:
/posts/[publicId]/page.tsx- Uses publicId parameter - Post edit:
/posts/[publicId]/edit/page.tsx- Uses publicId in URL, fetches by numeric ID internally - Old route:
/posts/[id]- REMOVED (replaced with publicId)
Statistics
- Alphabet: 62 characters (0-9, a-z, A-Z)
- Length: 12 characters
- Combinations: 62^12 ≈ 3.2 × 10^21 (3.2 sextillion)
- Collision Risk: Negligible (cryptographically secure via
gen_random_bytes())
Important Notes
- publicId is used for ALL public-facing URLs and sharing
- id (numeric) is still used internally for database relations (foreign keys, JOINs)
- All new posts automatically get a unique publicId via database default
- Migration 029 backfilled all existing posts with unique publicIds
post Image Carousel
The posts feed uses an interactive image carousel for posts with multiple images (1-5 images per post).
Key Components:
- ImageCarousel - components/posts/ImageCarousel.tsx
- Responsive carousel with navigation arrows and bullet indicators
- Supports 1-5 images per post
- Conditional rendering: only current + adjacent images for performance
- Keyboard accessible with ARIA labels
- Touch-friendly navigation buttons
Features:
-
Navigation Arrows:
- Left/Right chevron buttons for image navigation
- Wrap-around: last → first, first → last
- Visible on hover (desktop) or always (mobile)
- Semi-transparent background (bg-black/50)
-
Bullet Indicators:
- One bullet per image at bottom center
- Active bullet: filled white circle
- Inactive bullets: white border circle
- Clickable for direct navigation to specific image
-
Image Counter:
- Shows "X / Y" (current/total) in top-right corner
- Helps user understand position in carousel
-
Performance Optimization:
- Only renders current image + adjacent images (max 3 in DOM)
- Smooth opacity transitions (300ms)
- Lazy loading strategy through conditional rendering
Database Integration:
- Uses
postsWithAllMediainterface withimageUrls: string[] - Repository method:
getpostsFeedWithAllMedia(limit, offset) - SQL query aggregates all images per post with
ARRAY_AGG - Images ordered by
order_indexASC
Usage Example:
import { ImageCarousel } from '@/components/posts/ImageCarousel'
<ImageCarousel
images={['url1.jpg', 'url2.jpg', 'url3.jpg']}
alt="posts description"
/>
Responsive Design:
- Mobile: Arrows always visible with larger touch targets
- Desktop: Arrows visible on hover, bullets always visible
- Dark mode: Fully supported with consistent styling
Friends Feature (Venner)
Full documentation: docs/features/venner.md
Comprehensive friends/social network feature with friend requests, blocking, and privacy controls.
Quick reference:
import { friendshipsRepository } from '@/repositories'
// Check friendship status
const status = await friendshipsRepository.getFriendshipStatus(userId1, userId2)
const areFriends = await friendshipsRepository.areFriends(userId1, userId2)
// Components
<AddFriendButton userId={userId} variant="default" /> // 5 states: none/pending_sent/pending_received/accepted/blocked
Routes: /venner, /venner/foresporsler, /venner/blokkert, /venner/feed
Followers Feature
Full documentation: docs/features/followers-implementation.md
One-way follow system (like Twitter/Instagram). Friends auto-follow each other via database trigger.
Key difference from Friends: Followers don't get access to friends-only posts.
Quick reference:
import { followersRepository } from '@/repositories'
// Check follow status
const isFollowing = await followersRepository.isFollowing(userId, targetUserId)
const counts = await followersRepository.getFollowerCounts(userId)
// Components
<FollowButton userId={userId} /> // Blue → Gray
<FollowMeButton userId={userId} /> // Blue → Green (for modals)
NextAuth v5 (Beta) - Dual Authentication
Full documentation: docs/standards/authentication.md
Dual authentication: Google OAuth + Email/Password credentials. Config in src/lib/auth.ts.
Quick reference:
import { auth, signIn, signOut } from '@/lib/auth'
const session = await auth()
if (session?.user) {
session.user.id // string
session.user.role // string
session.user.isVerified // boolean
}
await signIn('credentials', { email, password, redirectTo: '/' })
await signIn('google', { redirectTo: '/' })
Key files: src/lib/auth.ts, src/lib/auth-adapter.ts, src/types/next-auth.d.ts
Environment: AUTH_SECRET, AUTH_URL, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
Brevo Email Service
The application uses Brevo (formerly Sendinblue) for sending transactional emails.
Configuration
- Package:
@getbrevo/brevoversion 3.0.1 - Location:
src/lib/email.ts- Email client and helper functions - API Key: Set
BREVO_API_KEYin environment variables - Free Tier: 300 emails/day (sufficient for small-medium applications)
- Get API Key: https://app.brevo.com/settings/keys/api
Available Functions
sendEmail(options: SendEmailOptions) - Generic email sender
import { sendEmail } from '@/lib/email'
await sendEmail({
to: 'user@example.com',
subject: 'Welcome!',
htmlContent: '<h1>Hello</h1>',
textContent: 'Hello' // optional fallback
})
sendVerificationEmail(email: string, token: string, userName: string) - Email verification
import { sendVerificationEmail } from '@/lib/email'
await sendVerificationEmail(
'user@example.com',
'verification-token-123',
'John Doe'
)
sendPasswordResetEmail(email: string, token: string, userName: string) - Password reset
import { sendPasswordResetEmail } from '@/lib/email'
await sendPasswordResetEmail(
'user@example.com',
'reset-token-456',
'John Doe'
)
Email Templates
- Verification Email: Styled HTML with 24-hour expiry link
- Password Reset: Styled HTML with 1-hour expiry link
- Both templates include text fallback for email clients without HTML support
Environment Variables
BREVO_API_KEY=your-brevo-api-key
EMAIL_FROM_NAME=Envoy
EMAIL_FROM_ADDRESS=noreply@your-domain.com
NEXT_PUBLIC_APP_URL=https://your-domain.com # Used for email links
Usage in Signup Flow
Email verification is integrated in the signup process:
- User submits signup form →
app/(auth)/signup/actions.ts - User created in database →
usersRepository.createUser() - Verification token generated →
verificationTokensRepository.createToken() - Email sent via Brevo →
sendVerificationEmail() - User clicks link in email →
/verify-email?token=xxx
Important Notes
- All emails use consistent styling with Envoy branding
- Debug logging in development mode (prefix:
[EMAIL]) - Returns boolean success status for error handling
- Rate limiting applied to signup prevents email spam
Database Connection (postgres-js + Supabase)
- Connection:
postgrespackage for direct PostgreSQL access - Configuration:
db/index.ts- postgres-js client withprepare: falsefor Supabase - Schema:
db/schema.ts- TypeScript interfaces (no ORM) - Queries: Raw SQL with template literals or parameterized queries
- Mapping: Helper functions convert snake_case (DB) to camelCase (TypeScript)
- User Schema: Includes
themecolumn for dark mode preference ('light' | 'dark' | 'system')
Database Access Pattern
// ✅ CORRECT - Use in repositories only
import { sql, mapUserRow } from '@/db'
const rows = await sql`SELECT * FROM users WHERE id = ${id}`
const user = mapUserRow(rows[0])
// With parameters for dynamic queries
const query = `UPDATE users SET name = $1 WHERE id = $2 RETURNING *`
const rows = await sql.unsafe(query, [name, id])
Timezone Handling (UTC Storage + Local Display)
Full documentation: docs/standards/timezone.md
Store timestamps in UTC, display in user's local timezone. User timezone stored in users.timezone column.
Quick reference:
import { formatTimestampInTimezone, getLocaleFromLanguage } from '@/lib/timezone'
// Format timestamp for display
const formatted = formatTimestampInTimezone(
post.createdAt,
user.timezone || 'Europe/Oslo',
getLocaleFromLanguage(user.language || 'nb')
)
Critical: postgres-js returns Date objects where components represent UTC but with local offset. Use Date.UTC() to correct before formatting. See full docs for details.
React Hydration Mismatch Prevention (i18n)
The application uses client-side language detection, which can cause hydration mismatch errors when server and client render different content.
The Problem
When a component uses useTranslation() from react-i18next:
- Server-side: Cannot detect browser language → uses fallback language (e.g., Norwegian
'nb') - Client-side: Detects browser language from
navigator.language(e.g., English'en') - Result: React hydration error because server HTML doesn't match client expectations
Example error:
Hydration mismatch: "Logg inn for å like" (server) vs "Log in to like" (client)
The Solution: suppressHydrationWarning
Add suppressHydrationWarning to any element that uses useTranslation():
'use client'
import { useTranslation } from 'react-i18next'
export default function MyComponent() {
const { t } = useTranslation('common')
return (
<div suppressHydrationWarning>
<button
aria-label={t('button.label')}
suppressHydrationWarning
>
{t('button.text')}
</button>
<span suppressHydrationWarning>{t('message')}</span>
</div>
)
}
Important: Apply suppressHydrationWarning to:
- The parent container (
<div>) - Any element with translated attributes (
aria-label,title,placeholder) - Any element with translated text content (
<span>,<p>, etc.)
Components Already Fixed
The following components have suppressHydrationWarning applied:
- components/post/LikeButton.tsx - Like button with translated tooltips
- components/post/VisibilityBadge.tsx - Visibility labels ("Gruppe", "Venner", etc.)
- components/post/CommentSection.tsx - Comment section headings and prompts
- components/post/postFeedCard.tsx - Post card with translated labels
- app/post/[id]/CommentsHeader.tsx - Uses
mountedstate pattern (alternative solution)
Alternative Solution: Mounted State Pattern
For components where you want to avoid hydration warnings entirely, use the "mounted state" pattern:
'use client'
import { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
export default function MyComponent() {
const { t } = useTranslation('common')
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
// Use fallback text until client is mounted
const getText = () => {
if (!mounted) return 'Loading...' // Fallback text
return t('message')
}
return <div>{getText()}</div>
}
Trade-off: This prevents hydration mismatch but causes a brief flash of fallback text on initial load.
Best Practices
- Use
suppressHydrationWarningfor most i18n components (simplest solution) - Use mounted state pattern if you need to avoid any hydration warnings in console
- Never hardcode language server-side for unauthenticated users (causes mismatch)
- Test with different browser languages to catch hydration issues
Related Configuration
- Language detection: src/lib/i18n.ts - i18next config with browser language detector
- Language context: src/contexts/LanguageContext.tsx - Client-side language state management
- Default language: Falls back to English (
'en') if browser language is not Norwegian
Next.js App Router Setup
The application uses Next.js 16 App Router with a hybrid structure:
App Directory (app/)
- Pages & Layouts: UI components and page-level logic
- Route Groups:
(auth)/for grouping without affecting URL structure - Server Components: Default for all components (opt-in to client with
'use client') - Server Actions: Preferred over API routes for mutations (e.g.,
app/(auth)/signup/actions.ts)
API Routes (app/api/)
- Located in
app/api/(Next.js requirement - cannot be insrc/) - Follow Next.js 16 route handler conventions
- Example:
app/api/auth/[...nextauth]/route.tsfor NextAuth
Key Patterns
- Server Components (default): Use for data fetching, reducing client bundle
- Client Components: Use
'use client'for interactivity (forms, state, effects) - Server Actions: Preferred for form submissions and mutations
'use server' export async function myAction(formData: FormData) { // Server-side logic with direct DB access } - Route Handlers: Use for REST API endpoints and webhooks
Layout & Responsive Design
Authentication & User Management
- Email handling: All user emails MUST be stored in lowercase
- Use Zod
.transform(val => val.toLowerCase())on email fields in all auth forms - Apply lowercase transformation in both signup and signin flows
- Ensures consistent email matching and prevents duplicate accounts
Styling Pattern
Combine Tailwind classes with utility function:
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs))
Responsive Design Standard
Full guide: docs/standards/responsive-design.md
3-Breakpoint Strategy (use these 95% of the time):
| Breakpoint | Size | Target |
|---|---|---|
| (default) | < 768px | Mobile phones |
md: | ≥ 768px | Tablets/iPad |
lg: | ≥ 1024px | Laptops |
// Standard pattern
<div className="p-4 md:p-6 lg:p-8">
<div className="text-sm md:text-base lg:text-lg">
<div className="grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
<div className="flex-col md:flex-row">
Touch targets: Minimum 44x44px (min-h-11 min-w-11)
Layout Structure
// Example responsive layout component
{/* Content */}
Common Responsive Patterns
- Navigation: Hamburger menu on mobile, full nav on desktop
- Sidebar: Hidden/drawer on mobile, fixed on desktop
- Typography: Smaller text on mobile (
text-sm md:text-base lg:text-lg) - Spacing: Tighter margins on mobile (
p-4 md:p-6 lg:p-8)
Profile/User Components
Profile Display
- Avatar/profile picture with responsive sizing
- User info layout that adapts to screen size
- Action buttons that stack on mobile, inline on desktop
Best Practices
- Use
aspect-ratiofor consistent image sizing - Implement proper image optimization with Next.js
<Image> - Consider touch targets (minimum 44x44px on mobile)
- Use
truncatefor long text on smaller screens
Deployment (Netlify)
This application is deployed to Netlify at: https://envoysupabase.netlify.app
Plan: Netlify Free Tier
Netlify Free Tier Limits
The application runs on Netlify's Free plan with the following limits:
- Bandwidth: 100 GB/month
- Build Minutes: 300 minutes/month
- Function Invocations: 125,000/month (for API routes/Server Actions)
- Edge Functions: 1 million invocations/month
- Storage: 10 GB
Important Notes:
- Limits are hard caps - site will be suspended if exceeded (can reactivate by upgrading)
- Notifications sent at 50%, 75%, 90%, and 100% of limits
- All features available on free tier (zero-config, edge caching, image optimization, etc.)
- No credit card required for free tier
Optimization Tips for Free Tier:
- Use Next.js Image optimization to reduce bandwidth usage
- Implement proper caching strategies (already configured)
- Monitor usage in Netlify Dashboard
- Consider static generation for pages where possible to reduce function invocations
Netlify Configuration
Netlify provides zero-configuration deployment for Next.js 16 applications:
- Automatic Adapter: Netlify's OpenNext adapter automatically configures the project
- No netlify.toml required: Netlify auto-detects Next.js and configures optimal settings
- Build Command:
npm run build(detected automatically) - Publish Directory:
.next(configured automatically)
Netlify-Specific Features
Automatic Optimizations:
- Fine-grained caching with Next.js Full Route Cache and Data Cache
- Static page responses cached at edge (automatic revalidation by path/tag)
- Image optimization via Netlify Image CDN
- Server Components and Server Actions fully supported
- Turbopack support (Next.js 16)
Deployment Options:
- Git-based: Push to GitHub/GitLab → Auto-deploy on commits
- Manual: Use Netlify CLI for local deployments
Environment Variables Setup
CRITICAL: All production environment variables must be configured in Netlify Dashboard.
Required Environment Variables
Reference .env.example for complete list. Key variables:
# Database (Supabase PostgreSQL)
DATABASE_URL=postgresql://user:password@host:5432/database
# NextAuth v5 (CRITICAL for auth to work)
AUTH_SECRET=<generate-with-openssl-rand-base64-32>
AUTH_URL=https://envoysupabase.netlify.app # MUST match deployment URL
# Google OAuth (if enabled)
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
# Supabase (file uploads)
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# Google Maps (geocoding)
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=your-google-maps-api-key
Setting Environment Variables
Option 1: Netlify Dashboard (Manual)
- Go to: https://app.netlify.com/sites/envoysupabase/configuration/env
- Click "Add a variable" for each environment variable
- Copy values from your local
.env.productionfile
Option 2: Netlify CLI (Recommended for bulk upload)
npm install -g netlify-cli
netlify login
netlify link
netlify env:import .env.production
See detailed instructions: DEPLOYMENT.md
Hostinger
The Domin is shopet at hostingers.com https://www.whatsmydns.net/#NS/myglimt.com
NextAuth v5 on Netlify
Important Notes:
-
AUTH_URL is critical: Must match your exact deployment URL
- Production:
https://envoysupabase.netlify.app - Custom domain:
https://your-custom-domain.com - Forgetting this causes redirect to localhost after login
- Production:
-
AUTH_SECRET: Generate with
openssl rand -base64 32- Must be consistent across deployments
- Never commit to git
-
Google OAuth Setup (if using Google login):
- Add authorized redirect URI in Google Cloud Console:
https://envoysupabase.netlify.app/api/auth/callback/google
- Add authorized redirect URI in Google Cloud Console:
-
Preview Deployments (branch previews):
- Set
AUTH_REDIRECT_PROXY_URLto stable deployment URL - Use same
AUTH_SECRETacross all preview deployments
- Set
Netlify Build Process
- Automatic Detection: Netlify detects Next.js 16 and applies optimal settings
- OpenNext Adapter: Auto-updates on each build (don't pin version)
- Build Steps:
npm install → npm run build → Deploy to CDN - Edge Caching: Static pages cached automatically with revalidation support
Common Deployment Issues
Issue: Redirects to localhost after login
Cause: AUTH_URL not set correctly in Netlify
Fix: Set AUTH_URL=https://envoysupabase.netlify.app in Netlify env vars
Issue: Google OAuth fails
Cause: Missing redirect URI or incorrect credentials Fix:
- Verify
GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRETin Netlify - Add
https://envoysupabase.netlify.app/api/auth/callback/googleto Google Cloud Console
Issue: Database connection fails
Cause: DATABASE_URL not set or incorrect
Fix: Verify Supabase connection string in Netlify env vars
Issue: Build fails
Cause: Missing dependencies or type errors
Fix: Run npm run build locally first to catch errors
Deployment Workflow
-
Local Development:
- Use
.env.localfor local environment variables - Test with
npm run dev
- Use
-
Pre-deployment:
- Run
npm run buildlocally to verify build success - Run
npm run lintto catch linting errors
- Run
-
Deploy:
- Push to GitHub → Netlify auto-deploys
- Or use Netlify CLI:
netlify deploy --prod
-
Post-deployment:
- Test authentication flow at production URL
- Verify database connections
- Check for any console errors
Important Notes
- Never commit
.env.localor.env.production(contains secrets) - Safe to commit:
.env.example(no real values) - Custom Domains: Update
AUTH_URLto match custom domain - Supabase Connection: Use Supabase connection pooler for serverless environments
- postgres-js Config:
prepare: falserequired for Supabase (already configured indb/index.ts)
Important Version Notes
- Next.js 16 and React 19 may have breaking changes from previous versions
- NextAuth v5 is in beta with different API than v4
- Tailwind CSS v4 uses
@importinstead of@tailwinddirectives