ADR 0003: Разделение схем БД (public / pm / blog)¶
Дата: 2026-06-06 Статус: Accepted
На какие вопросы отвечает¶
- Почему в одной PostgreSQL-базе живут три схемы и кто чем владеет?
- Может ли Payment Manager писать в таблицы Serverpod (или наоборот)?
- Как Serverpod читает баланс / историю транзакций, если они в
pm.*? - Почему Serverpod видит блог через
v_blog_post, а не напрямуюblog.posts? - Почему миграции трёх сервисов не конфликтуют друг с другом?
- Где «живёт» PII и почему его не видно из чужой схемы?
Контекст¶
OneWallet — это несколько сервисов поверх одной PostgreSQL 17:
- Auth Center (Serverpod) — схема
public.*, миграции черезserverpod generate+dart bin/main.dart --apply-migrations. Здесь профили, KYC, PII. - Payment Manager (Drizzle) — схема
pm.*, миграции черезdrizzle-kit migrate. Здесь intent'ы, маршруты, fee-rules, история TigerBeetle-проводок. - one_blog (SvelteKit + Drizzle) — схема
blog.*, свои миграции. CMS «What's on».
Если бы сервисы писали в общие таблицы, любая миграция одного ломала бы другого, а PII
из public.* протекал бы в платёжные и блог-таблицы. Нужна жёсткая граница владения.
Решение¶
Каждая схема имеет ровно одного владельца-писателя. Никто не пишет в чужую схему.
Кросс-чтение разрешено только через views в public, никогда не прямым доступом к
чужим таблицам.
| Схема | Владелец (write) | Инструмент миграций |
|---|---|---|
public.* |
Auth Center (Serverpod) | serverpod |
pm.* (11 таблиц) |
Payment Manager | drizzle-kit |
blog.* |
one_blog | drizzle-kit |
Serverpod читает чужие данные только через 5 views в public
(источник: projects/deploy/seeds/create-views.sql):
| View | Источник | Назначение |
|---|---|---|
v_user_tb_accounts |
pm.tb_account_map |
привязка user → TB-аккаунт (USER/MERCHANT/AGENT_WALLET) |
v_tx_history |
pm.tx_history |
история проводок без сырого PII (раскрывает только нужные поля) |
v_blog_category |
blog.categories |
справочник категорий блога |
v_blog_location |
blog.locations |
справочник локаций |
v_blog_post |
blog.post_groups ⋈ blog.posts |
карточки только published постов, фильтр по expires_at |
View — это явный контракт чтения: он сам решает, какие столбцы показать и в каком виде
(например, v_tx_history достаёт nfcTagId, lat, lon из attributes JSONB, но не
отдаёт внутренние/PII-поля). Доступ выдаётся только на чтение:
GRANT USAGE ON SCHEMA pm/blog + GRANT SELECT ON ALL TABLES.
Баланс при этом Serverpod берёт не из БД, а через nginx→PM
GET /accounts/{name}/balance— TigerBeetle единственный источник истины по деньгам (см. ADR 0001). Views покрывают только реляционные данные.
flowchart LR
subgraph PG[PostgreSQL 17]
PUB[(public.*\nServerpod)]
PM[(pm.*\nPayment Manager)]
BLOG[(blog.*\none_blog)]
V[public.v_* views]
PM -. SELECT .-> V
BLOG -. SELECT .-> V
V -. read-only .-> PUB
end
AC[Auth Center] -->|write| PUB
AC -->|read via views| V
PMW[Payment Manager] -->|write| PM
OB[one_blog] -->|write| BLOG
Пример¶
Endpoint Serverpod показывает пользователю карточки блога. Вместо запроса к blog.posts
(чужая схема) он читает public.v_blog_post через свою же ORM:
SELECT "postId", "slug", "title", "coverImageThumbUrl"
FROM public.v_blog_post
WHERE "categoryId" = $1 AND "locale" = 'th';
View уже отфильтровал черновики (status='published') и просроченные посты
(expires_at > now()), и отдал стабильные текстовые UUID — Serverpod не знает деталей
схемы blog.* и не сломается, если one_blog добавит туда колонку.
Последствия¶
- Изоляция PII. Сырой PII живёт только в
public.user_profile.encryptedPii(AES-256-GCM, см. ADR 0005). Чужие схемы и их сервисы к нему не имеют доступа, аv_tx_historyнамеренно не отдаёт PII-поля. - Независимые миграции.
drizzle-kit migrate(pm, blog) иserverpod ... --apply-migrations(public) не пересекаются: каждый трогает только свою схему. Это снимает класс конфликтов «две команды поменяли одну таблицу». - Явный контракт чтения. Изменения внутренней структуры
pm.*/blog.*не ломают читателей, пока сохранён контракт view; ломающее изменение видно как правкаcreate-views.sql. - Цена. При добавлении новых полей для чтения нужно править view и выдавать GRANT'ы;
views надо применять после миграций (
create-views.sqlидемпотентен, безопасно пере-запускать). - Граница доверия для записи. Любая запись в деньги идёт не в БД напрямую, а через
PM
POST /intentsс HMAC (см. ADR 0002, ADR 0004).
Ссылки¶
- SQL views:
projects/deploy/seeds/create-views.sql(а такжеprojects/onewallet_base/onewallet_base_server/scripts/create_views.sql) - Dev-документация: ../dev/03-data-and-schemas.md
- Смежные ADR: 0001 TigerBeetle, 0002 единый HMAC, 0004 единый Intent API, 0005 шифрование PII