> ## Documentation Index
> Fetch the complete documentation index at: https://sonamu.cartanova.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Context

> Extending Context for your project

You can extend the ContextExtend interface to add custom data to the Context for your project.

## Context Extension Overview

<CardGroup cols={2}>
  <Card title="Type Extension" icon="code">
    TypeScript type extension Autocomplete support
  </Card>

  <Card title="Middleware" icon="layer-group">
    Request preprocessing Custom data injection
  </Card>

  <Card title="Validation Logic" icon="shield-check">
    Auth/permission checks Business rule application
  </Card>

  <Card title="Common Data" icon="database">
    Session, tenant, etc. Request-scoped data
  </Card>
</CardGroup>

## Type Extension

### Custom User Type

```typescript theme={null}
// types/context.ts

// Extend base Context
declare module "sonamu" {
  interface ContextUser {
    id: number;
    email: string;
    username: string;
    role: "admin" | "manager" | "normal";

    // Add custom fields
    organizationId: number;
    departmentId: number;
    permissions: string[];
    lastLoginAt: Date;
    preferredLanguage: "ko" | "en" | "ja";
  }
}
```

### Custom Context Fields

```typescript theme={null}
// types/context.ts

declare module "sonamu" {
  interface ContextExtend {
    // Add custom fields
    tenant?: {
      id: number;
      name: string;
      subdomain: string;
      features: string[];
    };

    timezone: string;
    requestId: string;
  }
}
```

<Info>
  Use TypeScript's Declaration Merging to extend the `ContextExtend` interface.
  Base fields such as `request`, `reply`, `user`, `session`, and `locale` are already included in the `Context` type,
  so you don't need to redeclare them here. This enables IDE autocomplete and type checking.
</Info>

## Injecting Data with Middleware

### Auth Middleware

```typescript theme={null}
// middleware/auth.middleware.ts
import { FastifyRequest, FastifyReply } from "fastify";
import jwt from "jsonwebtoken";
import { Sonamu } from "sonamu";

export async function authMiddleware(request: FastifyRequest, reply: FastifyReply) {
  // Extract token from Authorization header
  const authHeader = request.headers.authorization;

  if (!authHeader || !authHeader.startsWith("Bearer ")) {
    return; // No token (optional auth)
  }

  const token = authHeader.substring(7);

  try {
    // Verify JWT token
    const payload = jwt.verify(token, process.env.JWT_SECRET!) as {
      userId: number;
      role: string;
      organizationId: number;
      departmentId: number;
      permissions: string[];
    };

    // Get user info from DB
    const rdb = Sonamu.getPuri("r");
    const user = await rdb.table("users").where("id", payload.userId).first();

    if (!user) {
      reply.status(401);
      throw new Error("Invalid token");
    }

    // Set user in Context
    const context = Sonamu.getContext();
    context.user = {
      id: user.id,
      email: user.email,
      username: user.username,
      role: user.role,
      organizationId: payload.organizationId,
      departmentId: payload.departmentId,
      permissions: payload.permissions,
      lastLoginAt: user.last_login_at,
      preferredLanguage: user.preferred_language,
    };

    // Update last activity time
    await rdb.table("users").where("id", user.id).update({ last_activity_at: new Date() });
  } catch (error) {
    reply.status(401);
    throw new Error("Invalid or expired token");
  }
}
```

### Tenant Middleware

```typescript theme={null}
// middleware/tenant.middleware.ts
import { FastifyRequest } from "fastify";
import { Sonamu } from "sonamu";

export async function tenantMiddleware(request: FastifyRequest) {
  const context = Sonamu.getContext();

  // Extract tenant info from subdomain or header
  const subdomain = getSubdomain(request.hostname);
  const tenantId = request.headers["x-tenant-id"] as string;

  if (!subdomain && !tenantId) {
    return; // No tenant info
  }

  const rdb = Sonamu.getPuri("r");

  // Get tenant
  const tenant = await rdb
    .table("tenants")
    .where((qb) => {
      if (subdomain) {
        qb.where("subdomain", subdomain);
      }
      if (tenantId) {
        qb.orWhere("id", parseInt(tenantId));
      }
    })
    .first();

  if (!tenant) {
    throw new Error("Tenant not found");
  }

  // Check tenant active status
  if (!tenant.is_active) {
    throw new Error("Tenant is inactive");
  }

  // Set tenant in Context
  context.tenant = {
    id: tenant.id,
    name: tenant.name,
    subdomain: tenant.subdomain,
    features: tenant.features || [],
  };
}

function getSubdomain(hostname: string): string | null {
  const parts = hostname.split(".");
  if (parts.length >= 3) {
    return parts[0];
  }
  return null;
}
```

### Locale Middleware

```typescript theme={null}
// middleware/locale.middleware.ts
import { FastifyRequest } from "fastify";
import { Sonamu } from "sonamu";

const SUPPORTED_LOCALES = ["ko", "en", "ja"];
const DEFAULT_LOCALE = "en";

export function localeMiddleware(request: FastifyRequest) {
  const context = Sonamu.getContext();

  // 1. URL query parameter
  const queryLocale = request.query?.locale as string;
  if (queryLocale && SUPPORTED_LOCALES.includes(queryLocale)) {
    context.locale = queryLocale;
    return;
  }

  // 2. User preference (logged in state)
  if (context.user?.preferredLanguage) {
    context.locale = context.user.preferredLanguage;
    return;
  }

  // 3. Accept-Language header
  const acceptLanguage = request.headers["accept-language"];
  if (acceptLanguage) {
    const lang = acceptLanguage.split(",")[0].split("-")[0];
    if (SUPPORTED_LOCALES.includes(lang)) {
      context.locale = lang;
      return;
    }
  }

  // 4. Default
  context.locale = DEFAULT_LOCALE;
}
```

### Request ID Middleware

```typescript theme={null}
// middleware/request-id.middleware.ts
import { FastifyRequest } from "fastify";
import { Sonamu } from "sonamu";
import crypto from "crypto";

export function requestIdMiddleware(request: FastifyRequest) {
  const context = Sonamu.getContext();

  // Client-provided Request ID or generate new
  const requestId = (request.headers["x-request-id"] as string) || crypto.randomUUID();

  context.requestId = requestId;

  // Add to response header as well
  context.reply.header("X-Request-Id", requestId);
}
```

## Registering Middleware

```typescript theme={null}
// server.ts
import fastify from "fastify";
import { authMiddleware } from "./middleware/auth.middleware";
import { tenantMiddleware } from "./middleware/tenant.middleware";
import { localeMiddleware } from "./middleware/locale.middleware";
import { requestIdMiddleware } from "./middleware/request-id.middleware";

const app = fastify();

// Register middleware (order matters!)
app.addHook("preHandler", requestIdMiddleware);
app.addHook("preHandler", tenantMiddleware);
app.addHook("preHandler", authMiddleware);
app.addHook("preHandler", localeMiddleware);

// Register API routes
// ...
```

## Practical Usage Examples

### Multi-tenant API

```typescript theme={null}
class ProductModel extends BaseModelClass {
  @api({ httpMethod: "GET" })
  async list(): Promise<Product[]> {
    const context = Sonamu.getContext();

    // Check tenant
    if (!context.tenant) {
      throw new Error("Tenant required");
    }

    const rdb = this.getPuri("r");

    // Query only tenant-specific data
    const products = await rdb.table("products").where("tenant_id", context.tenant.id).select("*");

    return products;
  }

  @api({ httpMethod: "POST" })
  async create(params: ProductSaveParams): Promise<{ productId: number }> {
    const context = Sonamu.getContext();

    if (!context.tenant) {
      throw new Error("Tenant required");
    }

    const wdb = this.getPuri("w");

    // Auto-add tenant ID
    const [product] = await wdb
      .table("products")
      .insert({
        ...params,
        tenant_id: context.tenant.id,
      })
      .returning("id");

    return { productId: product.id };
  }
}
```

### Permission-based Access Control

```typescript theme={null}
class BaseModelClass {
  protected requirePermission(permission: string): void {
    const context = Sonamu.getContext();

    if (!context.user) {
      context.reply.status(401);
      throw new Error("Authentication required");
    }

    if (!context.user.permissions.includes(permission)) {
      context.reply.status(403);
      throw new Error(`Permission denied: ${permission} required`);
    }
  }
}

class UserModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  async create(params: UserSaveParams): Promise<{ userId: number }> {
    // Check permission
    this.requirePermission("user:create");

    const wdb = this.getPuri("w");
    const [user] = await wdb.table("users").insert(params).returning("id");

    return { userId: user.id };
  }

  @api({ httpMethod: "DELETE" })
  async remove(userId: number): Promise<void> {
    this.requirePermission("user:delete");

    const wdb = this.getPuri("w");
    await wdb.table("users").where("id", userId).delete();
  }
}
```

### Internationalized Messages

```typescript theme={null}
// services/i18n.service.ts
import { Sonamu } from "sonamu";

const messages: Record<string, Record<string, string>> = {
  ko: {
    "user.not_found": "사용자를 찾을 수 없습니다",
    "user.created": "사용자가 생성되었습니다",
    "permission.denied": "권한이 없습니다",
  },
  en: {
    "user.not_found": "User not found",
    "user.created": "User created",
    "permission.denied": "Permission denied",
  },
  ja: {
    "user.not_found": "ユーザーが見つかりません",
    "user.created": "ユーザーが作成されました",
    "permission.denied": "権限がありません",
  },
};

export function t(key: string): string {
  const context = Sonamu.getContext();
  const locale = context.locale || "en";

  return messages[locale]?.[key] || key;
}

// Usage
class UserModel extends BaseModelClass {
  @api({ httpMethod: "GET" })
  async get(id: number): Promise<User> {
    const rdb = this.getPuri("r");
    const user = await rdb.table("users").where("id", id).first();

    if (!user) {
      // Internationalized error message
      throw new Error(t("user.not_found"));
    }

    return user;
  }
}
```

### Automated Audit Logging

```typescript theme={null}
class BaseModelClass {
  protected async audit(
    action: string,
    resourceType: string,
    resourceId: number,
    details?: any,
  ): Promise<void> {
    const context = Sonamu.getContext();
    const wdb = this.getPuri("w");

    await wdb.table("audit_logs").insert({
      user_id: context.user?.id,
      tenant_id: context.tenant?.id,
      action,
      resource_type: resourceType,
      resource_id: resourceId,
      details: JSON.stringify(details || {}),
      ip_address: context.request.ip,
      user_agent: context.request.headers["user-agent"],
      request_id: context.requestId,
      timestamp: new Date(),
    });
  }
}

class UserModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  async create(params: UserSaveParams): Promise<{ userId: number }> {
    const wdb = this.getPuri("w");
    const [user] = await wdb.table("users").insert(params).returning("id");

    // Auto audit log
    await this.audit("CREATE", "user", user.id, { email: params.email });

    return { userId: user.id };
  }

  @api({ httpMethod: "DELETE" })
  async remove(userId: number): Promise<void> {
    const rdb = this.getPuri("r");
    const user = await rdb.table("users").where("id", userId).first();

    await this.audit("DELETE", "user", userId, { username: user.username });

    const wdb = this.getPuri("w");
    await wdb.table("users").where("id", userId).delete();
  }
}
```

## Organization/Department Filtering

```typescript theme={null}
class DocumentModel extends BaseModelClass {
  @api({ httpMethod: "GET" })
  async list(): Promise<Document[]> {
    const context = Sonamu.getContext();

    if (!context.user) {
      throw new Error("Authentication required");
    }

    const rdb = this.getPuri("r");

    let query = rdb.table("documents");

    // Non-admins see only their organization/department
    if (context.user.role !== "admin") {
      query = query.where((qb) => {
        qb.where("organization_id", context.user!.organizationId)
          .orWhere("department_id", context.user!.departmentId)
          .orWhere("user_id", context.user!.id);
      });
    }

    return query.select("*");
  }
}
```

## Context-based Caching

```typescript theme={null}
class CacheService {
  private cache = new Map<string, any>();

  get(key: string): any {
    const context = Sonamu.getContext();
    const cacheKey = `${context.tenant?.id || "global"}:${key}`;
    return this.cache.get(cacheKey);
  }

  set(key: string, value: any, ttl: number = 3600): void {
    const context = Sonamu.getContext();
    const cacheKey = `${context.tenant?.id || "global"}:${key}`;
    this.cache.set(cacheKey, value);

    // Delete after TTL
    setTimeout(() => {
      this.cache.delete(cacheKey);
    }, ttl * 1000);
  }
}

// Usage
class ProductModel extends BaseModelClass {
  private cacheService = new CacheService();

  @api({ httpMethod: "GET" })
  async list(): Promise<Product[]> {
    // Tenant-specific cache
    const cached = this.cacheService.get("products:list");
    if (cached) {
      return cached;
    }

    const context = Sonamu.getContext();
    const rdb = this.getPuri("r");

    const products = await rdb.table("products").where("tenant_id", context.tenant?.id).select("*");

    this.cacheService.set("products:list", products);

    return products;
  }
}
```

## Cautions

<Warning>
  **Cautions when using custom Context**: 1. Define type extensions early in the project 2.
  Middleware execution order matters 3. Treat Context data as immutable 4. Consider encrypting
  sensitive information 5. Prevent memory leaks (avoid storing large data)
</Warning>

## Next Steps

<CardGroup cols={2}>
  <Card title="SonamuContext" icon="cube" href="/en/api-development/context/sonamu-context">
    Understanding Context structure
  </Card>

  <Card title="Getting Context" icon="hand" href="/en/api-development/context/getting-context">
    Using Sonamu.getContext()
  </Card>

  <Card title="@api Decorator" icon="at" href="/en/api-development/creating-apis/api-decorator">
    API basic usage
  </Card>

  <Card title="Middleware" icon="layer-group" href="/en/api-development/middleware">
    Request preprocessing
  </Card>
</CardGroup>
