---
title: "Найкращі практики"
description: "Патерни розробки, специфічні для Ring Platform — серверні дії, доступ до бази даних, автентифікація, платежі та тестування."
locale: "uk"
---
# Найкращі практики

Патерни розробки та конвенції, специфічні для Ring Platform.

### For founders

## Принципи розробки

### Якість понад кількість
Ring Platform — це мультитенантна white-label платформа, що забезпечує роботу понад 15 продакшен-клонів. Кожен патерн має однаково працювати як у середовищах з PostgreSQL (`k8s-postgres-fcm`), так і з Firebase. Рівень абстракції `DatabaseService` гарантує це — пишіть один раз у `db()`, працюйте будь-де.

### Безпека за конвенцією
- `userId` **завжди** отримується з `await auth()` у серверному коді, ніколи з параметрів, наданих клієнтом.
- Платіжні операції виконуються лише на сервері. HMAC-підписи WayForPay верифікують автентичність зворотних викликів.
- Завантаження файлів проходить через абстракцію `file()` (`@/lib/file`) — контроль доступу застосовується на рівні завантаження, ніколи не передбачається з боку клієнта.

### i18n не є опціональним
Усі рядки, орієнтовані на користувача, знаходяться в JSON-пакетах `locales/{locale}/`, які завантажуються через `next-intl`. Рядки безпосередньо в компонентах є ознакою поганого коду. Див. `docs/en/architecture/proxy-and-intl.mdx` для архітектури i18n.

### Поважайте режим бекенду
`DB_BACKEND_MODE` обирає адаптер бази даних під час виконання. Не імпортуйте `firebase-admin` або `postgres` безпосередньо в доменному коді. Використовуйте виключно `db()` з `@/lib/database`. Див. [Режими бекенду та бази даних](/en/docs/architecture/backend-modes-and-databases).

## Доступ до бази даних

### Синглтон `db()`

Основний інтерфейс бази даних — це синглтон `db()` з `@/lib/database`. Він автоматично ініціалізується та надає типізовані методи `*Doc`, які повертають об'єкти результату `{ success, data, error }`.

```typescript
import { db } from '@/lib/database'

// Створення
const result = await db().createDoc('entities', { name: 'Acme', type: 'technology' })
if (!result.success) throw result.error

// Читання за ID
const entity = await db().findDocById('entities', id)
if (!entity.success) throw entity.error

// Запит з фільтрами
const results = await db().queryDocs({
  collection: 'entities',
  filters: [{ field: 'status', operator: '==', value: 'active' }],
  orderBy: [{ field: 'name', direction: 'asc' }],
  pagination: { limit: 20, offset: 0 },
})

// Оновлення
await db().updateDoc('entities', id, { name: 'Acme Corp' })

// Видалення
await db().deleteDoc('entities', id)
```

> **Info**
> Синглтон `db()` переживає HMR Next.js завдяки `globalThis.__ringDatabaseService`. База даних ініціалізується один раз і використовує з'єднання повторно під час гарячих перезавантажень.

### Транзакції

Використовуйте `db().transaction()` для атомарних багатокрокових записів. Зворотний виклик транзакції отримує `IDatabaseTransaction` з методами `create`, `read`, `update`, `delete`. Помилки **викидаються** — не перевіряйте `result.success` всередині транзакції.

```typescript
import { db } from '@/lib/database'

await db().transaction(async (txn) => {
  const existing = await txn.read('usernames', usernameKey)
  if (existing) throw new Error('Username is taken')

  await txn.create('usernames', { userId, reservedAt: new Date() }, { id: usernameKey })
  await txn.update('users', userId, { username })
})
```

### Прямий доступ до сервісу (просунутий)

Для спеціалізованої маршрутизації бекенду або операцій з кількома адаптерами імпортуйте клас `DatabaseService` безпосередньо:

```typescript
import { getDatabaseService } from '@/lib/database/DatabaseService'
const svc = getDatabaseService()
await svc.initialize()
```

Див. [Режими бекенду та бази даних](/en/docs/architecture/backend-modes-and-databases) для деталей щодо вибору адаптера та синхронізації.

> **Warning**
> **Не** використовуйте сирі запити Firestore або pg у коді функцій. Кожна операція з базою даних має проходити через `DatabaseService`, щоб платформа могла змінювати бекенди без переписування бізнес-логіки.

## Серверні дії

Усі серверні дії знаходяться в `app/_actions/` і слідують узгодженому патерну.

### Конвенції

1. Директива `'use server'` на рівні файлу на початку.
2. `import { auth } from '@/auth'` для сесії — ніколи не приймайте `userId` від клієнта.
3. Повертайте об'єкти стану форми `{ success?: boolean, error?: string, message?: string }`.
4. Використовуйте `localizedRedirect` для навігації після успіху.
5. Валідація через Zod або вручну на межі.

```typescript
'use server'

import { auth } from '@/auth'
import { db } from '@/lib/database'
import { revalidatePath } from 'next/cache'
import { localizedRedirect } from '@/lib/i18n-server-redirect'
import type { Locale } from '@/i18n/shared'

export interface ActionResult {
  success?: boolean
  error?: string
  message?: string
  fieldErrors?: Record<string, string>
}

export async function createEntity(
  prevState: ActionResult | null,
  formData: FormData,
  locale: Locale
): Promise {
  const session = await auth()
  if (!session?.user?.id) return { error: 'Authentication required' }

  // Валідація
  const name = formData.get('name') as string
  if (!name?.trim()) return { error: 'Name is required' }

  // Запис
  const result = await db().createDoc('entities', {
    name: name.trim(),
    createdBy: session.user.id,
    createdAt: new Date(),
  })

  if (!result.success) return { error: result.error?.message ?? 'Creation failed' }

  revalidatePath(`/${locale}/entities`)
  localizedRedirect({ locale, href: '/entities' })
}
```

### Обробка помилок

- Повертайте рядки помилок для відображення у формі. Не викидайте помилки в діях, що повертають форми.
- Викидайте лише для невідновлюваних помилок (неправильна конфігурація, відсутні змінні середовища).
- Обгортайте `localizedRedirect` у try/catch — він викидає `NEXT_REDIRECT`, який має поширюватися.

> **Tip**
> Перевірте `app/_actions/vendor-actions.ts` для комплексного реального прикладу, що охоплює завантаження файлів (Vercel Blob через `file()`), вкладене створення + оновлення та багатокрокові потоки онбордингу.

## Автентифікація

### Серверна сторона (`@/auth`)

Імпортуйте `auth` з кореневого `@/auth` (не напряму з `next-auth` або `authjs` — Ring Platform надає налаштований екземпляр у `auth.ts`).

```typescript
import { auth } from '@/auth'

export async function getServerSession() {
  const session = await auth()
  return session
}
```

Виклик `auth()` доступний у:
- Серверних діях (`app/_actions/`)
- Обробниках маршрутів (`app/api/`)
- Серверних компонентах (асинхронний компонент)
- Проміжному ПЗ (middleware)

### Клієнтська сторона (`useSession`)

```typescript
'use client'

import { useSession } from '@/auth/client'

export function UserInfo() {
  const { data: session, status } = useSession()

  if (status === 'loading') return Loading...
  if (!session?.user) return Not signed in

  return {session.user.name}
}
```

### Конфігурація Auth.js (NextAuth v5)

Канонічна конфігурація auth знаходиться в `auth.ts` у корені проекту. Вона налаштовує:
- Провайдера облікових даних (email/пароль)
- Провайдера Google OAuth
- Стратегію JWT (з ротацією токенів оновлення)
- Адаптер PostgreSQL (`lib/auth/postgres-adapter.ts`)

Не створюйте додаткових файлів `auth.ts` у функціональних модулях. Розширюйте кореневу конфігурацію.

> **Warning**
> Ніколи не передавайте `userId` від клієнта до серверної дії або API-маршруту. Отримуйте його з `await auth()` на сервері. Див. `app/_actions/fcm.ts` для канонічного патерну — `userId отримується лише з auth() — ніколи не передавайте userId від клієнта`.

## Push-сповіщення (FCM)

Реєстрація та скасування реєстрації FCM-токенів відбуваються через серверні дії в `app/_actions/fcm.ts`. Відповідні операції з БД знаходяться в `lib/notifications/fcm-token-db.ts`.

```typescript
'use server'

import { auth } from '@/auth'
import { upsertFcmTokenForUser, unregisterFcmTokenForUser } from '@/lib/notifications/fcm-token-db'
import type { UpsertFcmTokenParams, UpsertFcmTokenResult } from '@/lib/notifications/fcm-token-db'

export async function upsertFcmToken(params: UpsertFcmTokenParams): Promise {
  const session = await auth()
  if (!session?.user?.id) return { error: 'Authentication required' }
  return upsertFcmTokenForUser(session.user.id, params)
}
```

Надсилайте сповіщення на стороні сервера за допомогою інструменту `ring-mcp_ring-fcm-send` або прямого обміну повідомленнями Firebase Admin. Ніколи не розкривайте серверні ключі FCM клієнту.

## Платежі (WayForPay)

Ring Platform використовує WayForPay як основний платіжний провайдер. Ключові патерни:

- **Уся логіка платежів виключно на сервері** — ніколи не розкривайте API-ключі або HMAC-секрети клієнту.
- **Ідемпотентний `orderReference`** — кожна спроба платежу має унікальний `orderReference` для запобігання дублюванню списань.
- **HMAC-верифікація** — зворотні виклики вебхуків верифікуються за допомогою HMAC MD5 перед обробкою.
- **Обробка 3DS** — перенаправлення WayForPay керуються через `components/store/checkout/`.

Див. `docs/en/architecture/payment-conductor.mdx` для повної архітектури платежів, потоку вебхуків та патернів повернення коштів.

## Межі серверних і клієнтських компонентів

- **За замовчуванням використовуйте серверні компоненти** — отримуйте дані та рендерте на сервері.
- **Опускайте клієнтські межі вниз** — обгортайте інтерактивні елементи (форми, мапи, реальний час) у листках `'use client'`.
- **Уникайте useEffect для отримання даних** — використовуйте серверні дії, обробники маршрутів або `useActionState` у React 19.

```typescript
// page.tsx — серверний компонент (за замовчуванням)
export default async function EntitiesPage({ params: { locale } }: Props) {
  const session = await auth()

  const result = await db().queryDocs({
    collection: 'entities',
    filters: [],
    pagination: { limit: 20 },
  })

  return 
}
```

## Завантаження файлів

Використовуйте абстракцію `file()` з `@/lib/file` замість прямого виклику API Vercel Blob або S3.

```typescript
import { file } from '@/lib/file'

const result = await file().upload(`entities/${entityId}/logo.webp', fileObject, {
  access: 'public',
  addRandomSuffix: false,
})
if (!result.success) throw new Error(result.error)
```

## Патерни тестування

Тести знаходяться в `__tests__/` і використовують Jest (з `@jest-environment jsdom` для тестів компонентів) або Vitest.

### Імітуйте зовнішні сервіси, а не DatabaseService

Тести Ring Platform імітують на межі — константи, `next/link`, пакунки перекладів — але **не** імітують `@/lib/database` з фейковим Firestore. Логіка, що залежить від бази даних, тестується або на реальній тестовій базі даних, або через інтеграційні тести, які використовують повний стек адаптерів.

> **Tip**
> Див. [Посібник з тестування](/en/docs/development/testing) для повної стратегії тестування та перегляньте реальні тестові файли в `__tests__/features/` (наприклад, `news-visibility-filter.test.ts`, `referral-commission.test.ts`) для патернів, що тестують чисту бізнес-логіку без імітації бази даних.

### Приклад тестування компонентів

```typescript
/**
 * @jest-environment jsdom
 */

import { render, screen } from '@testing-library/react'
import { NextIntlClientProvider } from 'next-intl'
import EntityCard from '@/components/EntityCard'
import type { Locale } from '@/i18n/shared'

// Імітація next/link
jest.mock('next/link', () => {
  return ({ children, href }: { children: React.ReactNode; href: string }) => (
    {children}
  )
})

const mockMessages = {
  'entities.card.viewDetails': 'View Details',
}

describe('EntityCard', () => {
  it('рендерить назву сутності', () => {
    const entity = { id: '1', name: 'Test Entity', type: 'technology' }

    render(
      
        
      
    )

    expect(screen.getByText('Test Entity')).toBeInTheDocument()
  })
})
```

## Продуктивність

### Патерни запитів до бази даних

- Використовуйте `db().queryDocs()` з `filters` та `pagination`, щоб обмежити результати на рівні бази даних — не отримуйте всі документи та не фільтруйте в коді застосунку.
- Використовуйте `db().findDocById()` для пошуку одного документа замість `queryDocs({ filters: [{ field: 'id', operator: '==', value: id }] })`.
- Кеш сутностей у `DatabaseService` забезпечує TTL 30 секунд для узгодженості читання після запису в колекції `entities` — ручне кешування не потрібне для базових випадків.

### Продуктивність React

- Серверні компоненти за замовчуванням — нульовий клієнтський JS для чистого рендерингу.
- `useActionState` (React 19) для керування станом форми в клієнтських компонентах — не потрібна зовнішня бібліотека стану.
- Динамічні імпорти з `next/dynamic` для важких клієнтських компонентів (мапи, редактори, графіки).

## Організація коду

- **`app/_actions/`** — усі серверні дії, один файл на домен.
- **`features/{domain}/services/`** — бізнес-логіка, яку можна викликати з серверних дій або обробників маршрутів.
- **`features/{domain}/lib/`** — доменно-специфічні чисті функції та утиліти.
- **`lib/database/`** — абстракція бази даних. Не додавайте сюди специфічні для функцій запити до бази даних.
- **`components/`** — лише спільні UI-компоненти. Специфічні для функцій компоненти знаходяться в `features/{domain}/components/`.

Див. [Структура коду](/en/docs/development/code-structure) для повної схеми директорій.

---

*Далі: [Структура коду](/en/docs/development/code-structure) — компонування App Router, модулі функцій та організація файлів.*
*Див. також: [Посібник з тестування](/en/docs/development/testing), [Режими бекенду та бази даних](/en/docs/architecture/backend-modes-and-databases)*
