Skip to content

Code Style Guide

This guide covers the coding standards and conventions used throughout the Sayr codebase. Following these guidelines ensures consistency and makes code reviews smoother.

Sayr uses Biome for linting and formatting. Run these commands before committing:

Terminal window
# Check for issues
pnpm lint
# Auto-fix issues
pnpm lint:fix
SettingValue
Indent styleTabs
Indent width3
Line width120 characters
Quote styleDouble quotes
SemicolonsAlways
Trailing commasES5 style
Arrow parenthesesAlways

Imports should be organized in three groups, separated by blank lines:

// 1. Workspace packages - always use @repo/* alias
import { db, schema, createTask } from "@repo/database";
import { Avatar, AvatarImage } from "@repo/ui/components/avatar";
import { cn } from "@repo/ui/lib/utils";
import { ensureCdnUrl } from "@repo/util";
// 2. External packages
import { Hono } from "hono";
import { useState, useEffect } from "react";
import { and, eq } from "drizzle-orm";
// 3. Local imports - use @/ alias (maps to src/ or app root)
import type { AppEnv } from "@/index";
import { SubWrapper } from "@/components/generic/wrapper";
TypeConventionExample
Variables/functionscamelCaseorgId, getTaskById, isAuthorized
Types/interfacesPascalCaseTaskContentProps, AppEnv
ComponentsPascalCaseTaskContent, SubWrapper
Route handlerscamelCase with prefixapiRouteAdminOrganization
Error codesUPPER_SNAKE_CASETASK_CREATION_FAILED
Database enumscamelCasestatusEnum, visibleEnum

Avoid any - the linter will warn you. Use explicit types instead:

// Bad
const handleData = (data: any) => { ... }
// Good
interface TaskData {
id: string;
title: string;
status: TaskStatus;
}
const handleData = (data: TaskData) => { ... }

Use import type for type-only imports to improve build performance:

// Good
import type { AppEnv } from "@/index";
import type { TaskWithLabels } from "@repo/database";
// Also good - mixed import
import { db, schema, type TaskWithLabels } from "@repo/database";

Use Drizzle’s built-in type inference for database types:

// Schema types
type Task = typeof schema.task.$inferSelect;
type NewTask = typeof schema.task.$inferInsert;
// Extended types are defined in packages/database/src/schema/index.ts
import type { TaskWithLabels, OrganizationWithMembers } from "@repo/database";

Follow this structure for React components:

"use client"; // if needed
// Imports organized as described above
import { Button } from "@repo/ui/components/button";
import { useState, useEffect } from "react";
// Interface definition
interface TaskContentProps {
task: TaskWithLabels;
open: boolean;
onOpenChange: (open: boolean) => void;
}
// Component
export function TaskContent({ task, open, onOpenChange }: TaskContentProps) {
// 1. Hooks first
const [state, setState] = useState<string>("");
const router = useRouter();
// 2. Effects
useEffect(() => {
// effect logic
}, []);
// 3. Handlers
const handleSubmit = async () => {
// handler logic
};
// 4. Render
return (
<div>
{/* component JSX */}
</div>
);
}

When creating context hooks, throw descriptive errors:

export function useLayoutOrganization() {
const context = useContext(LayoutOrganizationContext);
if (context === undefined) {
throw new Error(
"useLayoutOrganization must be used within RootProviderOrganization"
);
}
return context;
}

Use recordWideError for consistent error logging and tracking:

try {
const task = await createTask(data);
if (!task) {
await recordWideError({
name: "task.create.failed", // dotted notation for categorization
error: new Error("Task creation failed"),
code: "TASK_CREATION_FAILED", // UPPER_SNAKE_CASE code
message: "Failed to create task in database",
contextData: { orgId, title }, // relevant context for debugging
});
return c.json({ success: false, error: "Failed to create task" }, 500);
}
return c.json({ success: true, data: task });
} catch (err) {
await recordWideError({
name: "task.create.exception",
error: err,
code: "TASK_CREATION_EXCEPTION",
message: "Unexpected error creating task",
contextData: { orgId, title },
});
return c.json({ success: false, error: "An unexpected error occurred" }, 500);
}

Use dotted notation for error names that follows this pattern:

{resource}.{action}.{result}

Examples:

  • task.create.failed
  • organization.update.slug_taken
  • label.delete.not_found
  • member.invite.email_exists

Hono route handlers should follow this pattern:

apiRouteAdminOrganization.post("/create-label", async (c) => {
// 1. Setup tracing and error handling
const traceAsync = createTraceAsync();
const recordWideError = c.get("recordWideError");
const session = c.get("session");
// 2. Parse request body
const { org_id: orgId, wsClientId, name, color } = await c.req.json();
// 3. Check permissions
const isAuthorized = await traceOrgPermissionCheck(session?.userId || "", orgId, "content.manageLabels");
if (!isAuthorized) {
return c.json({ success: false, error: "Permission denied" }, 401);
}
// 4. Perform the operation
const label = await traceAsync(
"label.create",
() => createLabel({ organizationId: orgId, name, color }),
{ description: "Creating label" }
);
// 5. Handle errors
if (!label) {
await recordWideError({
name: "label.create.failed",
error: new Error("Label creation failed"),
code: "LABEL_CREATION_FAILED",
message: "Failed to create label",
contextData: { orgId, name },
});
return c.json({ success: false, error: "Failed to create label" }, 500);
}
// 6. Broadcast updates (if real-time)
broadcast(orgId, "admin", { type: "CREATE_LABEL", data: label }, wsClientId);
// 7. Return success
return c.json({ success: true, data: label });
});

When data changes need to be pushed to clients in real-time:

import { broadcast, broadcastPublic, broadcastIndividual } from "@/routes/ws";
// Broadcast to a specific channel (e.g., "tasks", "admin")
broadcast(orgId, "tasks", { type: "UPDATE_TASK", data: task }, excludeSocket);
// Broadcast to the public channel (for public board viewers)
broadcastPublic(orgId, { type: "UPDATE_TASK", data: task });
// Broadcast to a specific user's socket
broadcastIndividual(socket, { type: "NOTIFICATION", data: notification }, orgId);

WebSocket message types are defined as constants:

type: "CREATE_TASK" as WSBaseMessage["type"]
type: "UPDATE_TASK" as WSBaseMessage["type"]
type: "DELETE_TASK" as WSBaseMessage["type"]

Wrap important operations with tracing for observability:

const traceAsync = createTraceAsync();
const result = await traceAsync(
"operation.name", // span name
() => performOperation(), // the operation to trace
{
description: "Human readable description",
data: { relevantContext },
onSuccess: (result) => ({
description: "Operation succeeded",
data: { resultId: result.id },
}),
}
);
  • Complex business logic that isn’t immediately obvious
  • Workarounds for known issues (include issue link if available)
  • Public API functions (use JSDoc)
/**
* Fetches all tasks for a given organization with related data.
*
* @param orgId - The organization ID
* @returns Array of tasks with labels, assignees, and comments
*/
export async function getTasksByOrganizationId(
orgId: string
): Promise<TaskWithLabels[]> {
// implementation
}

Before committing, ensure:

  1. pnpm lint passes with no errors
  2. pnpm check-types passes with no type errors
  3. Any new code follows the patterns in this guide
  4. Complex logic has appropriate comments
  5. No console.log statements left in production code