Database Architecture
Single database, multi-schema architecture — the mandatory pattern for all NaaP plugins and services.
The Golden Rule#
Every plugin and service MUST use the unified
@naap/databasepackage. Never create per-plugin databases, Prisma schemas, or PrismaClient instances.
NaaP uses a single PostgreSQL database with multiple schemas for data isolation. All 60+ models across 8 schemas are defined in one Prisma schema file and shared via the @naap/database package.
Why This Architecture#
| Concern | Multi-DB (old, removed) | Single-DB Multi-Schema (current) |
|---|---|---|
| Docker containers | 10 separate PostgreSQL containers | 1 container |
| Connection pools | 10 x pool_size connections | 1 shared pool |
| Schema management | 7 separate Prisma schemas | 1 unified schema |
| Migrations | Run independently per plugin | Run once from packages/database |
| Cross-plugin queries | Impossible (different DBs) | Supported (same DB, Prisma relations) |
| Dev setup | ~30s to start all containers | ~3s for 1 container |
| Ops / backup | Back up 10 databases | Back up 1 database |
| Memory usage | ~500MB (10 x PostgreSQL) | ~50MB (1 x PostgreSQL) |
Architecture Diagram#
| 1 | ┌─────────────────────────────────────────────────────────┐ |
| 2 | │ Application Layer │ |
| 3 | │ │ |
| 4 | │ ┌──────────┐ ┌───────────┐ ┌──────────┐ ┌──────────┐ │ |
| 5 | │ │ base-svc │ │ community │ │ daydream │ │ gateway │ │ |
| 6 | │ └────┬─────┘ └─────┬─────┘ └────┬─────┘ └────┬─────┘ │ |
| 7 | │ └─────────────┬────────────┘ │ │ |
| 8 | │ │ │ │ |
| 9 | │ ┌──────▼──────────────────────────▼──┐ │ |
| 10 | │ │ @naap/database │ │ |
| 11 | │ │ Singleton PrismaClient │ │ |
| 12 | │ │ multiSchema: enabled │ │ |
| 13 | │ └──────────────┬──────────────────────┘ │ |
| 14 | └─────────────────────────────┼──────────────────────────┘ |
| 15 | │ |
| 16 | ┌─────────▼─────────┐ |
| 17 | │ naap-db │ |
| 18 | │ PostgreSQL 16 │ |
| 19 | │ │ |
| 20 | │ ┌──────────────┐ │ |
| 21 | │ │ public │ │ Core (User, Auth, Team, RBAC) |
| 22 | │ ├──────────────┤ │ |
| 23 | │ │ plugin_ │ │ Community Hub |
| 24 | │ │ community │ │ |
| 25 | │ ├──────────────┤ │ |
| 26 | │ │ plugin_ │ │ Daydream Video |
| 27 | │ │ daydream │ │ |
| 28 | │ ├──────────────┤ │ |
| 29 | │ │ plugin_ │ │ My Wallet |
| 30 | │ │ wallet │ │ |
| 31 | │ ├──────────────┤ │ |
| 32 | │ │ plugin_ │ │ Gateway, Dashboard, |
| 33 | │ │ gateway ... │ │ Capacity, Dev API |
| 34 | │ └──────────────┘ │ |
| 35 | └────────────────────┘ |
Schema Map#
| PostgreSQL Schema | Package/Plugin | Example Models |
|---|---|---|
public | base-svc | User, Team, Plugin, Auth, RBAC |
plugin_community | community | CommunityProfile, CommunityPost, CommunityComment |
plugin_wallet | my-wallet | WalletConnection, WalletTransactionLog |
plugin_dashboard | my-dashboard | Dashboard, DashboardUserPreference |
plugin_daydream | daydream-video | DaydreamSettings, DaydreamSession |
plugin_gateway | gateway-manager | Gateway, GatewayOrchestratorConnection |
plugin_capacity | capacity-planner | CapacityRequest, CapacitySoftCommit |
plugin_developer_api | developer-api | DevApiAIModel, DevApiKey |
How It Works#
1. Single Source of Truth#
All models are defined in one Prisma schema file:
packages/database/prisma/schema.prismaEvery model uses @@schema("plugin_xxx") to assign it to a PostgreSQL schema:
| 1 | model CommunityPost { |
| 2 | id String @id @default(uuid()) |
| 3 | title String |
| 4 | content String |
| 5 | authorId String |
| 6 | createdAt DateTime @default(now()) |
| 7 | updatedAt DateTime @updatedAt |
| 8 | |
| 9 | @@schema("plugin_community") // ← This is mandatory |
| 10 | } |
2. One Prisma Client, Shared by All#
The @naap/database package exports a singleton client:
| 1 | // packages/database/src/index.ts |
| 2 | import { PrismaClient } from './generated/client'; |
| 3 | |
| 4 | export const prisma = globalForPrisma.prisma ?? new PrismaClient({...}); |
Every plugin imports from this package — never from a local generated client:
// plugins/community/backend/src/db/client.ts
import { prisma } from '@naap/database';
export const db = prisma;3. Schema Isolation at the PostgreSQL Level#
The docker/init-schemas.sql file runs on first boot and creates all schemas:
| 1 | CREATE SCHEMA IF NOT EXISTS plugin_community; |
| 2 | CREATE SCHEMA IF NOT EXISTS plugin_wallet; |
| 3 | CREATE SCHEMA IF NOT EXISTS plugin_daydream; |
| 4 | -- ... etc |
Tables are created inside their schema automatically by Prisma's multiSchema feature.
Mandatory Rules#
These rules must be followed by every plugin and service:
Rule 1: No Per-Plugin Prisma Schemas#
❌ plugins/my-plugin/backend/prisma/schema.prisma ← NEVER
✅ packages/database/prisma/schema.prisma ← ALWAYSRule 2: No Per-Plugin Generated Clients#
❌ import { PrismaClient } from '../generated/client' ← NEVER
❌ import { PrismaClient } from '@prisma/client' ← NEVER
✅ import { prisma } from '@naap/database' ← ALWAYSRule 3: No Per-Plugin Database Containers#
| 1 | # ❌ NEVER — separate containers per plugin |
| 2 | gateway-db: |
| 3 | image: postgres:16-alpine |
| 4 | ports: ["5433:5432"] |
| 5 | |
| 6 | # ✅ ALWAYS — one unified container |
| 7 | database: |
| 8 | image: postgres:16-alpine |
| 9 | ports: ["5432:5432"] |
| 10 | volumes: |
| 11 | - ./docker/init-schemas.sql:/docker-entrypoint-initdb.d/01-init-schemas.sql |
Rule 4: Every Model Must Have @@schema()#
| 1 | // ❌ NEVER — model without schema annotation |
| 2 | model MyWidget { |
| 3 | id String @id @default(uuid()) |
| 4 | } |
| 5 | |
| 6 | // ✅ ALWAYS — explicit schema |
| 7 | model MyWidget { |
| 8 | id String @id @default(uuid()) |
| 9 | @@schema("plugin_my_plugin") |
| 10 | } |
Rule 5: Single DATABASE_URL#
All .env files must point to the same database:
Connection String#
| Environment | URL |
|---|---|
| Local dev | postgresql://postgres:postgres@localhost:5432/naap |
| Production | Set via DB_PASSWORD env var in docker-compose.production.yml |
Next Steps#
- Database Setup Tutorial — Step-by-step guide for adding a schema
- Database Plugin Example — Full worked example
- AI Prompt: Database Compliance — Copy-paste prompt for AI assistants