Skip to content

Code Style

!!! info "TL;DR" Use ESLint and Prettier (auto-run via pre-commit hooks). Follow TypeScript strict mode, validate all inputs with Zod, use Tailwind for styling with consistent class ordering. Never bypass RLS in client code.

Linting and Formatting

Canviq enforces code style via:

  • ESLint: JavaScript/TypeScript linting with strict rules
  • Prettier: Code formatting (120-character lines, single quotes, semicolons)
  • lint-staged: Pre-commit hooks auto-fix on commit

Run linting manually:

npm run lint         # Check for issues
npm run lint -- --fix  # Auto-fix issues
npm run format       # Format with Prettier

Pre-commit hooks will automatically run eslint --fix and prettier --write on staged files. If CI fails due to linting, fix locally and re-push.

TypeScript

Canviq uses TypeScript in strict mode with these conventions:

  • Explicit return types for exported functions
  • No any types — use unknown and type guards
  • Zod schemas for validation at system boundaries (API routes, form submissions, external data)
  • Database types auto-generated via npx supabase gen types typescript --local

Example:

import { z } from 'zod'
import type { Database } from '@/types/database'

const submissionSchema = z.object({
  title: z.string().min(5).max(200),
  description: z.string().min(20).max(5000),
})

export async function createSubmission(
  input: z.infer<typeof submissionSchema>
): Promise<Database['public']['Tables']['submissions']['Row']> {
  // Implementation
}

Tailwind CSS

Use Tailwind utility classes with consistent ordering:

  1. Layout (display, position, width, height)
  2. Spacing (margin, padding)
  3. Typography (font, text, leading)
  4. Visual (background, border, shadow)
  5. Interactivity (hover, focus, transition)

Example:

<button className="flex items-center gap-2 rounded-lg bg-coral-500 px-4 py-2 text-sm font-medium text-white shadow-sm transition-colors hover:bg-coral-600 focus:ring-2 focus:ring-coral-300">
  Submit
</button>

Use design tokens from tailwind.config.ts (colors, spacing, typography) instead of arbitrary values.

Component Patterns

Server Components by Default

Use Server Components unless you need client-side interactivity:

// app/[locale]/page.tsx (Server Component)
import { getTranslations } from 'next-intl/server'

export default async function HomePage() {
  const t = await getTranslations('submissions')
  return <h1>{t('title')}</h1>
}

Client Components for Interactivity

Mark client components with 'use client':

// components/vote-button.tsx
'use client'
import { useState } from 'react'

export function VoteButton({ submissionId }: { submissionId: string }) {
  const [voted, setVoted] = useState(false)
  // Implementation
}

Server Actions for Mutations

Use Server Actions for form submissions:

// app/[locale]/submit/actions.ts
'use server'
export async function createSubmission(formData: FormData) {
  // Validation, authentication, database mutation
}

Supabase Client Usage

Use the correct client variant:

// Browser (client components)
import { createBrowserClient } from '@/lib/supabase/client'

// Server (server components, Route Handlers, Server Actions)
import { createServerClient } from '@/lib/supabase/server'

// Admin (service role, bypasses RLS — use sparingly)
import { createAdminClient } from '@/lib/supabase/admin'

Never use the admin client in client-side code or bypass RLS policies without a documented security reason.

Security Conventions

  • Validate all inputs with Zod schemas at API boundaries
  • Never log secrets (API keys, passwords, full auth tokens)
  • Use parameterized queries (Supabase client handles this automatically)
  • Log admin actions to the audit_log table
  • Check authentication before mutations (use auth() from @/lib/auth)

Example:

'use server'
import { createServerClient } from '@/lib/supabase/server'
import { auth } from '@/lib/auth'

export async function deleteSubmission(id: string) {
  const session = await auth()
  if (!session) throw new Error('Unauthorized')

  const supabase = await createServerClient()
  const { error } = await supabase.from('submissions').delete().eq('id', id)

  if (error) throw error
}

Internationalization

All user-facing strings must use next-intl:

// Server components
const t = await getTranslations('submissions')

// Client components
const t = useTranslations('submissions')

// Usage
t('status.planned') // Looks up messages/{locale}.json

Never hardcode English strings in components.

What's next