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
anytypes — useunknownand 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:
- Layout (display, position, width, height)
- Spacing (margin, padding)
- Typography (font, text, leading)
- Visual (background, border, shadow)
- 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_logtable - 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¶
- Learn about testing patterns
- Understand the pull request process
- Review Architecture Decision Records