---
title: "Data Model"
description: "How Ring stores users, entities, opportunities, commerce, and wallet data — JSONB-first PostgreSQL, hybrid platform_settings, email CRM via jsonb-collection"
locale: "en"
---
# Data Model

> **Info**
> **One article, two lenses.** Use the **Founder** / **Developer** tabs in the docs sidebar to filter this page. Shared sections stay visible for both audiences; wrapped blocks show concepts or integration detail respectively.

Ring Platform stores almost all application data in **PostgreSQL** using a **JSONB-first document shape**: every row is `id` + `data` + timestamps. That gives founders a flexible product model (add fields without migrations for every tweak) and gives developers a single `db()` contract across every Ring clone.

## What lives in the database?

| Domain | What founders configure | Typical clone scenarios |
|--------|-------------------------|-------------------------|
| **Users & auth** | Roles, profiles, credit balance | Member portals, gated content, admin consoles |
| **Entities** | Organizations, vendor profiles, verified listings | Directory, marketplace sellers, NGO profiles |
| **Opportunities** | Jobs, grants, bounties, RFPs | Job board, grant matcher, project marketplace |
| **Store & orders** | Multi-vendor catalog, inventory, checkout | White-label shop, B2B catalog, subscription boxes |
| **Wallet & payments** | Ledger, WayForPay/Stripe, membership credits | Token-gated perks, prepaid credits, affiliate payouts |
| **Messaging & notify** | Conversations, in-app + FCM push | Buyer–seller chat, opportunity alerts |
| **Content & CRM** | News, email sequences, AI matcher hooks | Community hub, publisher ring, opportunity notifications |
| **Platform settings** | AI provider keys, branding, web3 oracle | SUPERadmin namespace config per clone |

### For founders

## Why this model matters for your Ring clone

When you **ringize** a deployment, you are not buying a rigid CRM schema. You inherit a **proven marketplace + community skeleton** that already connects:

- **People** (users with roles from visitor → superadmin)
- **Organizations** (entities — your "vendor", "partner", or "chapter" records)
- **Demand** (opportunities — anything you'd post as a listing others apply to)
- **Commerce** (store products, orders, PaymentConductor settlements)
- **Trust** (reviews, verification flags, confidential tiers)
- **Operator config** (platform settings namespaces — AI, branding, web3)

### Founder scenarios (generalized)

  
- **[Regional opportunity network](/docs/features/opportunities.md)** — Entities represent local businesses; opportunities are grants and contracts; AI matcher notifies relevant members.

  
- **[Multi-vendor marketplace](/docs/features/store.md)** — Each entity can sell through the store module; orders and inventory share one Postgres cluster per clone.

  
- **[Membership + credits](/docs/features/membership.md)** — User JSONB holds `credit_balance`; wallet transactions provide an audit trail for top-ups and spends.

  
- **[Confidential deal room](/docs/features/security.md)** — Role `confidential` plus entity/opportunity visibility flags — same tables, stricter layout gates.

  You do **not** need separate databases per feature. Postgres + JSONB lets each clone emphasize store, opportunities, or community without forking the codebase. Schema SSOT: `data/schema.sql` (v4.0.3).

### For developers

## JSONB document contract

Every core collection table follows the same physical layout (see `data/schema.sql`):

{`CREATE TABLE IF NOT EXISTS users (
    id VARCHAR(255) PRIMARY KEY,
    data JSONB NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_users_data_gin ON users USING GIN (data);`}

**Design rules:**

- **`id`** — Firebase UID, UUID string, or domain key (e.g. lowercase username in `usernames`).
- **`data`** — all domain fields; query with `data->>'field'` and GIN indexes for hot paths.
- **Triggers** — `updated_at` maintained cluster-wide; LISTEN/NOTIFY hooks for realtime adapters.

### Hybrid exception: `platform_settings`

Most tables are pure JSONB-in-`data`. **`platform_settings`** is the only hybrid table with top-level `secrets` and `updated_by` columns:

{`CREATE TABLE IF NOT EXISTS platform_settings (
    id VARCHAR(64) PRIMARY KEY,
    data JSONB NOT NULL DEFAULT '{}',
    secrets JSONB NOT NULL DEFAULT '{}',
    updated_by VARCHAR(255),
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);`}

`PostgreSQLAdapter` splits writes automatically: public config → `data`, API keys → `secrets`, actor → `updated_by`. Services consume via `db().readDoc` / `createDoc` / `updateDoc`:

| Module | Namespace examples | Path |
|--------|-------------------|------|
| Platform settings | `ai`, `branding`, matcher | `features/admin/platform-settings/platform-settings-service.ts` |
| Ring token oracle | `web3` | `features/wallet/services/native-token-oracle.ts` |

  Never write raw SQL or instantiate a private `pg.Pool` for `platform_settings`. The adapter hybrid path is the only supported write surface.

### DatabaseService (application API)

All server code goes through `db()` from `@/lib/database` — never raw SQL in route handlers (except PostGIS via `getSharedPgPool()`).

{`import { initializeDatabase, db } from '@/lib/database'

await initializeDatabase()

const user = await db().readDoc('users', userId)
if (!user.success) throw user.error

await db().createDoc('entities', {
  name: 'Acme Cooperative',
  userId,
  type: 'organization',
  status: 'active',
}, { id: entityId })

const list = await db().queryDocs({
  collection: 'opportunities',
  filters: [{ field: 'status', operator: '==', value: 'open' }],
  orderBy: [{ field: 'created_at', direction: 'desc' }],
  pagination: { limit: 20 },
})`}

> **Warning**
> `readDoc` / `queryDocs` return `{ success, data, error }`. **`error` is an `Error` object**, not a string. Throw on failure in services; Server Actions may map to `{ error: string }` for forms only.

### Email CRM via jsonb-collection

The email CRM feature does not open its own database connection. `features/email-crm/lib/jsonb-collection.ts` wraps `db().*Doc`:

| Helper | Delegates to |
|--------|-------------|
| `readDoc` | `db().readDoc` |
| `upsertDoc` | `db().readDoc` + `updateDoc` or `createDoc` |
| `queryDocs` | `db().queryDocs` |
| `deleteDoc` | `db().deleteDoc` |

Repositories (`jsonb-contact-repository.ts`, `jsonb-draft-repository.ts`, etc.) import from `jsonb-collection.ts` — same JSONB row shape as other collections.

### Entity-relationship map (logical)

```mermaid
erDiagram
    USERS ||--o{ ENTITIES : creates
    USERS ||--o{ OPPORTUNITIES : posts
    USERS ||--o{ ORDERS : places
    USERS ||--o{ WALLET_TRANSACTIONS : initiates

    ENTITIES ||--o{ OPPORTUNITIES : offers
    ENTITIES ||--o{ REVIEWS : receives
    ENTITIES }o--|| VENDOR_PROFILES : has

    OPPORTUNITIES ||--o{ APPLICATIONS : receives
    OPPORTUNITIES ||--o{ AI_MATCHES : generates

    STORE_PRODUCTS ||--o{ ORDER_ITEMS : contains
    STORE_PRODUCTS }o--|| VENDOR_PROFILES : sold_by

    ORDERS ||--o{ ORDER_ITEMS : includes
    ORDERS ||--o{ PAYMENTS : requires

    PLATFORM_SETTINGS ||--|| USERS : updated_by

    USERS {
        varchar id PK
        jsonb data
        timestamptz created_at
    }

    PLATFORM_SETTINGS {
        varchar id PK
        jsonb data
        jsonb secrets
        varchar updated_by
    }

    ENTITIES {
        varchar id PK
        jsonb data
        timestamptz created_at
    }

    OPPORTUNITIES {
        varchar id PK
        jsonb data
        timestamptz created_at
    }
```

### Apply schema to a fresh database

### Locate SSOT

Single file: `data/schema.sql` (v4.0.3). Do not use deprecated `scripts/postgres-schema.sql`.

### Apply on cluster or local Postgres

{`psql -h localhost -U ring_user -d ring_platform -f data/schema.sql`}

### Set backend mode

Production rings use `DB_BACKEND_MODE=k8s-postgres-fcm`. See [Backend modes and databases](./backend-modes-and-databases).

### Query and index patterns

- Filter JSONB: `(data->>'status') = 'open'` with btree expression indexes on hot keys.
- Full-text: opportunities table includes search vectors (see schema comments).
- Geospatial: PostGIS via `lib/geolocation/geolocation-service.ts` and `getSharedPgPool()` — not private pools.

Deep dive: `AI-CONTEXT/concepts/database/query-patterns.json` and [Discovery & mutation sync](./discovery-mutation-sync).

## Related documentation

- [architecture/data-validation](/docs/architecture/data-validation.md) — Next-step: Zod schemas at the route boundary before JSONB lands.

- [architecture/backend-modes-and-databases](/docs/architecture/backend-modes-and-databases.md) — Prerequisite: when Postgres is primary vs Firebase-full; shared pool SSOT.

- [backend/k8s-postgres-fcm](/docs/backend/k8s-postgres-fcm.md) — Deep-dive: PostGIS setup, pool tuning, K8s deployment.

- [architecture/authentication](/docs/architecture/authentication.md) — Same-workflow: Auth.js tables live in the same JSONB model.

- [architecture/payment-conductor](/docs/architecture/payment-conductor.md) — Depends-on: orders and payments JSONB plus webhook idempotency.

- [features/entities](/docs/features/entities.md) — See-also: product-facing entity profiles on the same collection pattern.

- [features/admin-wiki](/docs/features/admin-wiki.md) — Same-workflow: wiki_pages / wiki_links / wiki_events use id + data JSONB like other collections.
