MyGlimt
Loading...

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

Standards

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.theme column
  • 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 management
  • components/ThemeToggle.tsx - Theme toggle UI components
  • app/api/user/theme/route.ts - API endpoint to update user theme preference
  • app/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-900 on bg-white)
  • Dark mode: Light text on dark backgrounds (dark:text-gray-100 on dark: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/, NOT src/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 ID
    • getUserByEmail(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 user
    • userExists(email) - Check if user exists
  • friendshipsRepository: Friendship CRUD operations (raw SQL)

    • createFriendRequest(requesterId, addresseeId) - Send friend request
    • acceptFriendRequest(friendshipId, userId) - Accept friend request
    • rejectFriendRequest(friendshipId, userId) - Reject friend request
    • blockUser(requesterId, addresseeId) - Block user
    • unblockUser(friendshipId, userId) - Unblock user
    • removeFriend(friendshipId, userId) - Remove friend
    • getFriendshipStatus(userId1, userId2) - Get friendship status
    • areFriends(userId1, userId2) - Check if users are friends
    • isBlocked(requesterId, addresseeId) - Check if blocked
    • isBlockedBidirectional(userId1, userId2) - Check blocking both ways
    • getFriends(userId, options) - Get friends list with search/sort
    • getPendingRequests(userId) - Get incoming friend requests
    • getSentRequests(userId) - Get outgoing friend requests
    • getBlockedUsers(userId) - Get blocked users list
    • getFriendsCount(userId) - Get total friends count
    • getPendingRequestsCount(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 user
    • isFollowing(followerId, followingId) - Check if user is following another
    • getFollowing(userId, limit, offset) - Get list of users this user follows
    • getFollowers(userId, limit, offset) - Get list of users following this user
    • getFollowerCounts(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 ID
    • getPostWithMediaByPublicId(publicId) - Get post with media by public_id
    • getPostWithVisibilityCheck(postId, viewerId) - Check access
    • getPostByUserIdWithVisibility(profileUserId, viewerId, limit, offset) - User's posts
    • getFriendsFeed(userId, limit, offset) - Friends-only feed
    • canViewPost(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.ts contains TypeScript interfaces
  • Types: Manually defined (User, Account, Session, VerificationToken, Friendship, Post, PostMedia)
  • Mapping: Helper functions in db/index.ts convert 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 postsWithAllMedia interface with imageUrls: string[]
  • Repository method: getpostsFeedWithAllMedia(limit, offset)
  • SQL query aggregates all images per post with ARRAY_AGG
  • Images ordered by order_index ASC

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/brevo version 3.0.1
  • Location: src/lib/email.ts - Email client and helper functions
  • API Key: Set BREVO_API_KEY in 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:

  1. User submits signup form → app/(auth)/signup/actions.ts
  2. User created in database → usersRepository.createUser()
  3. Verification token generated → verificationTokensRepository.createToken()
  4. Email sent via Brevo → sendVerificationEmail()
  5. 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: postgres package for direct PostgreSQL access
  • Configuration: db/index.ts - postgres-js client with prepare: false for 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 theme column 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:

  1. The parent container (<div>)
  2. Any element with translated attributes (aria-label, title, placeholder)
  3. Any element with translated text content (<span>, <p>, etc.)

Components Already Fixed

The following components have suppressHydrationWarning applied:

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

  1. Use suppressHydrationWarning for most i18n components (simplest solution)
  2. Use mounted state pattern if you need to avoid any hydration warnings in console
  3. Never hardcode language server-side for unauthenticated users (causes mismatch)
  4. 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 in src/)
  • Follow Next.js 16 route handler conventions
  • Example: app/api/auth/[...nextauth]/route.ts for 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):

BreakpointSizeTarget
(default)< 768pxMobile phones
md:≥ 768pxTablets/iPad
lg:≥ 1024pxLaptops
// 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-ratio for consistent image sizing
  • Implement proper image optimization with Next.js <Image>
  • Consider touch targets (minimum 44x44px on mobile)
  • Use truncate for 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:

  1. Git-based: Push to GitHub/GitLab → Auto-deploy on commits
  2. 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)

  1. Go to: https://app.netlify.com/sites/envoysupabase/configuration/env
  2. Click "Add a variable" for each environment variable
  3. Copy values from your local .env.production file

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:

  1. 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
  2. AUTH_SECRET: Generate with openssl rand -base64 32

    • Must be consistent across deployments
    • Never commit to git
  3. Google OAuth Setup (if using Google login):

    • Add authorized redirect URI in Google Cloud Console:
      https://envoysupabase.netlify.app/api/auth/callback/google
      
  4. Preview Deployments (branch previews):

    • Set AUTH_REDIRECT_PROXY_URL to stable deployment URL
    • Use same AUTH_SECRET across all preview deployments

Netlify Build Process

  1. Automatic Detection: Netlify detects Next.js 16 and applies optimal settings
  2. OpenNext Adapter: Auto-updates on each build (don't pin version)
  3. Build Steps:
    npm install → npm run build → Deploy to CDN
    
  4. 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:

  1. Verify GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in Netlify
  2. Add https://envoysupabase.netlify.app/api/auth/callback/google to 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

  1. Local Development:

    • Use .env.local for local environment variables
    • Test with npm run dev
  2. Pre-deployment:

    • Run npm run build locally to verify build success
    • Run npm run lint to catch linting errors
  3. Deploy:

    • Push to GitHub → Netlify auto-deploys
    • Or use Netlify CLI: netlify deploy --prod
  4. Post-deployment:

    • Test authentication flow at production URL
    • Verify database connections
    • Check for any console errors

Important Notes

  • Never commit .env.local or .env.production (contains secrets)
  • Safe to commit: .env.example (no real values)
  • Custom Domains: Update AUTH_URL to match custom domain
  • Supabase Connection: Use Supabase connection pooler for serverless environments
  • postgres-js Config: prepare: false required for Supabase (already configured in db/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 @import instead of @tailwind directives
Claude Documentation - Envoy