---
title: "Best Practices"
description: "Ring Platform development patterns — db(), provider SSOT, Server Actions, auth, and performance guards."
locale: "en"
---
# Best Practices

Development patterns and conventions specific to Ring Platform v1.6.4.

> **Info**
> Use **Founder** / **Developer** tabs in the docs sidebar to filter this page.

### For founders

## Development principles

### Quality over quantity
Ring Platform is a multi-tenant white-label platform powering production clones. Every pattern must work across PostgreSQL-primary (`k8s-postgres-fcm`) and Firebase-backed deployments. The `DatabaseService` abstraction ensures this — write to `db()` once, run anywhere.

### Security by convention
- `userId` is **always** derived from `await auth()` in server code, never from client-supplied parameters.
- Payment operations run server-side only. WayForPay HMAC signatures verify callback authenticity.
- File uploads go through `file()` abstraction (`@/lib/file`) — access controls are enforced at the upload layer.

### i18n is not optional
All user-facing strings live in `locales/{locale}/` JSON bundles loaded by `next-intl`. See [Proxy and i18n](/docs/architecture/proxy-and-intl.md).

### Respect the backend mode
`DB_BACKEND_MODE` selects the database adapter at runtime. Do not import `firebase-admin` or `postgres` directly in domain code. See [Backend modes and databases](/docs/architecture/backend-modes-and-databases.md).

### For developers

## Database access

### The `db()` singleton

The primary database interface is `db()` from `@/lib/database`. It auto-initializes and provides typed `*Doc` methods returning `{ 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

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 },
})
```

> **Info**
> The `db()` singleton survives Next.js dev HMR via `globalThis.__ringDatabaseService`. Collections map to PostgreSQL tables through `PostgreSQLAdapter` field mappings in `lib/database/adapters/PostgreSQLAdapter.ts` — aligned with `data/schema.sql`. Do not assume ad-hoc table names; use the collection keys the adapter routes (e.g. `platform_settings`, `entities`, `users`).

### `platform_settings` and namespaced config

Platform-wide settings (AI matcher, branding, feature flags) persist in the `platform_settings` collection/table. Access via `db()` — not raw SQL:

```typescript
const settings = await db().findDocById('platform_settings', 'ai')
if (!settings.success) throw settings.error
```

Secrets columns are masked in admin GET handlers; never embed production keys in MDX or client bundles.

### PostGIS escape hatch only

For spatial queries the doc-model cannot express, use the shared pool:

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

const pool = await getSharedPgPool()
const { rows } = await pool.query(
  `SELECT id FROM entities WHERE ST_DWithin(location, ST_MakePoint($1, $2)::geography, $3)`,
  [lng, lat, radiusM]
)
```

`getSharedPgPool()` in `lib/database/shared-pg-pool.ts` returns the connected `PostgreSQLAdapter` pool after `initializeDatabase()`. **`new Pool(` is allowed only under `lib/database/`** — the SSOT script enforces this.

### Transactions

Use `db().transaction()` for atomic multi-step writes. Errors **throw** inside the callback.

```typescript
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 })
})
```

> **Warning**
> Do **not** use raw Firestore or pg queries in feature code except PostGIS via `getSharedPgPool()`. Every other database operation goes through `DatabaseService` so backends can switch without rewriting business logic.

## Provider SSOT script

`scripts/validate-provider-ssot.sh` gates duplicate-fetch regressions. Run before merging provider or hook changes:

```bash
./scripts/validate-provider-ssot.sh
```

| # | Gate | Rule |
|---|------|------|
| 1 | `useUnreadCount` | Allowed only in `notification-provider`, `use-unread-count.ts`, tests |
| 2 | `useCreditBalance(` | Allowed only in `credit-balance-provider`, `use-credit-balance.ts`, tests |
| 3 | `useCreditHistory(` | Allowed only in `credit-history-provider`, `use-credit-history.ts`, `wallet-wrapper`, tests |
| 4 | `fetch('/api/vendor/status')` | Allowed only in `hooks/use-vendor-status.ts` |
| 5 | `'/api/wallet/credit/balance'` | Allowed only in `hooks/use-credit-balance.ts` |
| 6 | `SessionProvider` | Exactly one `export function SessionProvider` — SSOT: `features/auth/components/session-provider.tsx` |
| 7 | `new Pool(` | Allowed only under `lib/database/` |

## Client single-flight pattern

When a hook performs a one-shot bootstrap fetch (not a long-lived subscription), use **module-scope single-flight + short TTL** keyed by `userId`:

| Hook | TTL | Endpoint |
|------|-----|----------|
| `use-vendor-status.ts` | 30s | `GET /api/vendor/status` |
| `use-credit-balance.ts` (bootstrap only) | 5s | `GET /api/wallet/credit/balance` |

Pattern: check cache → return in-flight promise → fetch → store. Manual `refresh()` bypasses bootstrap cache. See `hooks/HOOKS-README.md` § P4.

Provider-owned streams (unread count, credit balance live updates) still use context — single-flight is for **deduping mount-time duplicates**, not replacing providers.

## Server Actions

All Server Actions live in `app/_actions/`.

1. File-level `'use server'` at the top.
2. `import { auth } from '@/auth'` — never accept `userId` from the client.
3. Return form-state objects `{ success?, error?, message? }`.
4. Use `localizedRedirect` for navigation after success.
5. Zod or manual validation at the boundary.

> **Tip**
> See `app/_actions/vendor-actions.ts` for file uploads via `file()`, nested create + update, and multi-step onboarding.

## Authentication

### Server-side

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

export async function getServerSession() {
  return auth()
}
```

### Client-side

```typescript
'use client'
import { useSession } from '@/auth/client'
// Or useAuth() from hooks/use-auth.ts for signOut + FCM cleanup
```

Canonical config: root `auth.ts`. Session wrapper: `features/auth/components/session-provider.tsx` with `refetchInterval={15 * 60}`, `refetchOnWindowFocus={false}`, `refetchWhenOffline={false}`.

> **Warning**
> Never pass `userId` from the client to a Server Action or API route. See `app/_actions/fcm.ts` for the canonical pattern.

## Server vs client boundaries

- **Default to Server Components** — fetch via services or `db()` on the server.
- **Push client boundaries down** — forms, maps, realtime leaves only.
- **Avoid useEffect for data fetching** — prefer Server Actions, Route Handlers, or `useActionState`.

## File uploads

```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?.message ?? 'Upload failed')
```

## Testing

Tests live in `__tests__/`. Mock at boundaries (constants, `next/link`, i18n) — do not mock `@/lib/database` with fake Firestore for unit tests. See [Testing guide](/docs/development/testing.md).

## Code organization

| Path | Purpose |
|------|---------|
| `app/_actions/` | Server Actions, one file per domain |
| `features/{domain}/services/` | Business logic callable from actions or routes |
| `lib/database/` | Database abstraction — no feature-specific queries |
| `components/` | Shared UI; feature components in `features/{domain}/components/` |

See [Code structure](/docs/development/code-structure.md) for provider matrix and P4 hooks.

## Related documentation

  
- **[Code structure](/docs/development/code-structure.md)** — App Router layout, provider matrix, SessionProvider SSOT path.

  
- **[Performance](/docs/development/performance.md)** — Caching layers, pool rules, SSOT enforcement.

  
- **[Backend modes](/docs/architecture/backend-modes-and-databases.md)** — `DB_BACKEND_MODE` and adapter selection.

  
- **[Data validation](/docs/architecture/data-validation.md)** — Zod schemas and API boundaries.
