Database Consolidation Analysis
Database Consolidation Analysis
Document Version: 1.0 Date: 2026-02-01 Status: Initial Analysis Related ADR: ADR-005 Related Issue: #65
Executive Summary
The Agile Network platform currently operates with 26 separate PostgreSQL databases distributed across microservices. This analysis identifies significant consolidation opportunities that could reduce operational complexity by approximately 70-80% while improving data consistency and reducing cross-database query overhead.
Key Findings
- 26 databases analyzed with 330+ total migrations
- 8 core domain areas identified for consolidation
- High duplication in user profile, media management, and event scheduling schemas
- Cross-database joins currently impossible, requiring application-level data aggregation
- Opportunity: Consolidate to 3-5 bounded context databases
Recommended Consolidation Strategy
| Target Database | Source Databases (Count) | Estimated Reduction |
|---|---|---|
| User & Identity DB | celebrity-db, fan-db, group-profile-db (3) | 67% reduction |
| Content & Media DB | content-db, media-db, shoutout-db, mux-livestream-db (4) | 75% reduction |
| Commerce DB | stripe-db, subscriptions-db, wallet-db, transaction-db, inventory-db (5) | 80% reduction |
| Events & Streaming DB | broadcast-db, stream-db, onsite-event-db, class-catalog-db (4) | 75% reduction |
| Platform Services DB | notification-service-db, message-board-db, sse-db, tags-db (4) | 75% reduction |
| Infrastructure DB | encryption-db, dwolla-db, tracking-db (3) | 67% reduction |
Retention as-is: flyway-db (legacy monolith schema), charity-db (minimal), custom-tixr-db (stub)
Database Inventory
1. User & Identity Domain
peeq-celebrity-db (19 migrations)
Purpose: Expert/athlete profile management
Core Tables: - celebrity_profiles -
Expert profiles (bio, display_name, status, metadata) - PK:
celebrity_user_id (uuid, references Keycloak) - Columns:
bio, first_name, last_name, created_on, status -
celebrity_profile_status_history - Audit trail for profile
state changes - celebrity_profile_image - Profile images
(URL storage, not binary) - celebrity_profile_tags -
Many-to-many with tags service - celebrity_profile_banners
- Banner images (V16) - profile_short_links - Custom URL
slugs (V15) - cameo_offer, shoutout_offer -
Pricing and availability
Dependencies: - External: Keycloak (celebrity_user_id) - Cross-database: tags-db (via UUIDs in celebrity_profile_tags.tags)
Migration Highlights: - V1: Initial schema - V8: Migrated celebrity_user_id from code to UUID - V17: Added store_code for commerce integration - V18: Added display_weight for sorting/ranking
peeq-fan-db (12 migrations)
Purpose: Consumer profile and following relationships
Core Tables: - fan_profiles - Fan user
profiles - PK: fan_user_id (uuid, references Keycloak) -
Columns: created_on, meta_data (JSON) - fan_follows -
Fan-to-celebrity following relationships - Composite key: fan_user_id +
celeb_user_id (MD5 hash stored as id) - Columns: followed_on,
unfollowed_on, notifications (JSON array) - Tracks follow/unfollow
lifecycle - fan_follow_changes - Audit log for following
changes - Columns: occurred_on, change (JSON)
Dependencies: - External: Keycloak (fan_user_id) - Implicit: celebrity-db (celeb_user_id references celebrity profiles)
Key Observations: - Minimal schema (only 3 tables after 12 migrations) - Highly normalized with separate audit table - No actual foreign keys to celebrity-db (cross-database constraint impossible)
peeq-group-profile-db (11 migrations)
Purpose: Groups/teams/organizations (collection of celebrities)
Core Tables: - group_profiles -
Team/organization profiles - PK: profile_id (uuid) -
Columns: name, description, group_profile_type, is_approved, is_deleted,
owner_id, parent_group_profile_id - Self-referential for hierarchical
groups - profile_thumbnails - Group images (many-to-one) -
group_profile_tags - Tag definitions -
group_profile_celebs - Celebrity definitions -
profile_tags_association - Group-to-tag many-to-many -
profile_celebs_association - Group-to-celebrity
many-to-many
Dependencies: - Implicit: celebrity-db (celeb_user_id in profile_celebs_association) - Implicit: tags-db (tag concepts)
Consolidation Potential: HIGH - Could merge into unified User domain database with celebrity and fan profiles.
2. Content & Media Domain
peeq-content-db (7 migrations)
Purpose: Generic file storage metadata
Core Tables: - files - File metadata
(not binary storage) - PK: id (varchar) - Columns:
content_id, content_length, mime_type, name, owner_id, content_md5 - No
binary data - references external storage (S3/CDN)
Key Observations: - Extremely simple schema (1 table) - Does NOT store file content, only metadata - Likely redundant with media-db
peeq-media-db (27 migrations)
Purpose: Rich media file management with Mux integration
Core Tables: - files - Media file
metadata - PK: content_id (uuid) - Columns: owner_id,
content_length, content_md5, mime_type, content_secret - Rich media
fields: duration, height, width, type (photo/video), state - Integration
fields: media_info (JSON), mux_info (JSON for video transcoding) -
thumbnail_for - References another file as thumbnail -
files_price_history - Pricing audit trail (referenced in
schema but not in V1) - History tables for state transitions (likely in
later migrations)
Dependencies: - External: Mux (video transcoding) - External: S3/CDN (actual file storage)
Overlap with content-db: - Both have
files table with similar structure - media-db is superset
with video-specific fields - Strong consolidation
candidate
peeq-shoutout-db (23 migrations)
Purpose: Personalized video message streams
Core Tables: - mux_stream - Shoutout
video stream metadata - PK: id (uuid) - Columns:
created_by_id, inventory_item_id, stream_state, stream_key -
Integration: mux_info (JSON), inventory_info (JSON)
Dependencies: - External: Mux (livestream infrastructure) - Cross-database: inventory-db (inventory_item_id)
Consolidation Potential: HIGH - Could merge into Content & Media DB with media-db
peeq-mux-livestream-db (15 migrations)
Purpose: Live broadcast stream management
Core Tables: - mux_livestreams -
Livestream metadata - PK: livestream_id (uuid) - Columns:
name, description, inventory_info (JSON), group_id, created_by -
Indexed: group_id, created_on
Dependencies: - External: Mux - Cross-database: inventory-db (inventory_info references items), group-profile-db (group_id)
Overlap: - Similar to shoutout-db (both manage Mux streams) - Could consolidate into unified Video Streaming schema
3. Commerce Domain
peeq-stripe-db (13 migrations)
Purpose: Payment processing and exchange rates
Core Tables: - exchange_rates -
Currency/token conversion rates - PK: id (uuid) - Columns:
price, amount, name, keywords (array), meta_data, deleted_on
Dependencies: - External: Stripe API
Key Observations: - Single table for currency exchange - Likely used for virtual currency/tokens
peeq-subscriptions-db (14 migrations)
Purpose: Recurring subscription management
Core Tables: - subscriptions -
Subscription plans - PK: id (uuid) - Columns: name,
description, recurring_payment, auto_expires, created_by_id -
series - Content series for subscriptions - PK:
id (uuid) - Columns: name, description, created_by_id
Dependencies: - Implicit: celebrity-db (created_by_id likely references celebs)
peeq-wallet-db (4 migrations)
Purpose: User balance and transaction management
Core Tables: - balance_transaction -
User account balances - PK: id (bigint) - Columns: user_id,
balance - create_transaction - Transaction records - PK:
id (bigint) - Columns: user_id, amount, is_debit, item_id,
description, meta_data -
create_transaction_related_transactions - Transaction graph
(refunds, splits)
Dependencies: - Cross-database: user-db (user_id), inventory-db (item_id)
peeq-transaction-db (7 migrations)
Purpose: Purchase transaction records
Core Tables: - transactions - Purchase
history - PK: id (uuid) - Columns: user_id, item_id,
purchase_id, price, quantity, created_by_id, created_on
Overlap with wallet-db: - Both track transactions - wallet-db has balance management - transaction-db has purchase-specific fields - Strong consolidation candidate
peeq-inventory-db (28 migrations)
Purpose: Purchasable item catalog
Core Tables: - inventory_item - Items
available for purchase - PK: id (uuid) - Unique:
item_id (varchar) - Columns: type, state, price, quantity,
created_by_id - Array columns: associated_series,
associated_subscriptions, associated_tags, keywords, restricted_users -
Subscription fields: recurring_payment, auto_expires, is_refundable,
limit_per_fan - inventory_item_price_history - Price change
audit - inventory_item_quantity_history - Stock change
audit - inventory_item_state_history - State transition
audit
Dependencies: - Cross-database: subscriptions-db (associated_subscriptions), tags-db (associated_tags), celebrity-db (created_by_id)
Consolidation Potential: HIGH - Central to commerce domain, should consolidate with stripe/wallet/transaction databases
4. Events & Streaming Domain
peeq-broadcast-db (23 migrations)
Purpose: Live broadcast event scheduling
Core Tables: - broadcasts - Broadcast
event definitions - PK: id (uuid) - Columns: id_str,
id_type (polymorphic ID pattern), name, description - Timestamps:
created_on, starts_on, stops_on - Integration: mux_info (JSON),
meta_data (JSON) - Array: associated_item_ids (inventory items)
Dependencies: - Cross-database: inventory-db (associated_item_ids), mux-livestream-db (mux_info)
peeq-stream-db (13 migrations)
Purpose: Generic stream management
Core Tables: - streams - Stream
definitions - Identical schema to broadcasts table -
PK: id (uuid) - Columns: id_str, id_type, created_by_id,
starts_on, stops_on, name, description, mux_info, meta_data,
associated_item_ids
Duplication: - Exact duplicate of broadcast-db structure - Critical consolidation target - appears to be unnecessary separation
peeq-onsite-event-db (3 migrations)
Purpose: Physical event management and check-ins
Core Tables: - on_site_events -
In-person event definitions - PK: id (uuid) - Columns:
name, description, event_date, event_sign_up_type, group_id,
inventory_state, inventory_info (JSON) - Indexed: event_date,
sign_up_type, created_by, inventory_state, group_id -
user_event_checkins - Attendance tracking - PK:
id (uuid) - Columns: user_id, event_id, checked_in_at - FK:
event_id → on_site_events
Dependencies: - Cross-database: group-profile-db (group_id), user-db (user_id for check-ins)
peeq-class-catalog-db (18 migrations)
Purpose: Educational class/course management
Core Tables: - classes - Course
definitions - PK: id (uuid) - Columns: name, description,
prerequisites, blog_id, group_id, created_by_id -
class_thumbnails - Course images -
class_details - Lesson/module breakdown - Columns: name,
description, display_order, class_id -
class_associated_tags - Course categorization -
class_badges - Achievement tracking - sessions
- Class sessions/offerings - PK: id (uuid) - Columns:
attendance_format, inventory_info (JSON), original_price -
session_instructors - Instructor assignments
(many-to-many)
Dependencies: - Cross-database: group-profile-db (group_id), inventory-db (inventory_info), celebrity-db (instructor_id)
5. Platform Services Domain
peeq-notification-service-db (10 migrations)
Purpose: System notifications
Core Tables: - Minimal schema:
SELECT 1 from pg_indexes; (stub/placeholder in V1) - Actual
notification tables likely in later migrations
peeq-message-board-db (5 migrations)
Purpose: Discussion boards/chat rooms
Core Tables: - message_boards - Board
definitions - PK: id (uuid) - Columns: id_str, id_type,
name, description, starts_on, stops_on, associated_item_ids (array) -
members - Board membership - Columns: message_board_id,
user_id, added_on, removed_on, name, meta_data - messages -
Chat messages - Columns: message_board_id, members_id, message_content,
message_content_type, meta_data
Dependencies: - Cross-database: user-db (user_id in members), inventory-db (associated_item_ids)
peeq-sse-db (3 migrations)
Purpose: Server-Sent Events group management
Core Tables: - groups - SSE event
groups - PK: id (uuid) - Columns: id_str, id_type,
created_by_id, meta_data, associated_item_ids (array) -
members - Group membership - Columns: group_id, user_id,
added_on, meta_data
Overlap: - Similar structure to message-board-db (groups + members pattern) - Could consolidate into unified real-time messaging schema
peeq-tags-db (6 migrations)
Purpose: Centralized tagging/categorization
Core Tables: - tags - Tag definitions -
PK: id (uuid) - Columns: name, description, type, color,
thumbnail, keywords (array), associated_tags (array), meta_data
Dependencies: - Referenced by: celebrity-db, inventory-db, class-catalog-db, group-profile-db (all use tag UUIDs)
Strategic Importance: - Cross-cutting concern used by many services - Should remain centralized or replicate to consolidated databases
6. Infrastructure Domain
peeq-encryption-db (2 migrations)
Purpose: Key management vault
Core Tables: - vault_namespaces -
Encryption key namespaces - PK: namespace_id (varchar) -
Columns: public_key, private_key, version, created_on, created_by -
vault_secrets - Encrypted secrets storage - PK:
secret_id (varchar) - Columns: namespace_id, key, value
(encrypted), created_on, updated_on - Unique index: (namespace_id,
key)
Security Concern: - Stores encryption keys and secrets - Should remain isolated for security boundary - Retain as separate database
peeq-dwolla-db (4 migrations)
Purpose: Dwolla payment vault (duplicate of encryption-db)
Core Tables: - vault_namespaces -
Identical to encryption-db - vault_secrets - Identical to
encryption-db
Duplication: - Exact copy of encryption-db schema - Separated for Dwolla-specific secrets - Could consolidate with encryption-db but maintain namespace separation
node-tracking-db (9 migrations)
Purpose: Analytics and tracking
Core Tables (MySQL, not PostgreSQL): -
client - Client fingerprinting (browser, OS, UTM tracking)
- environment - Multi-tenant environment config -
impression - Page view tracking - link - Short
URL management - link_clicks - Click tracking -
link_templates - URL template definitions
Technology Difference: - MySQL database (not PostgreSQL like others) - Significant schema difference - Analytics-focused, not transactional - Retain separately due to technology difference
7. Legacy Monolith
peeq-flyway-db (75 migrations)
Purpose: Original monolith schema (event sourcing pattern)
Core Tables (51+ tables): - celebrity,
fan, moderator - User entities (duplicates
newer microservice schemas) - audio, photo,
video - Media entities with categories -
conference, live, one_on_one_meet
- Event/meeting entities - message,
broadcast_message, collaborate_message -
Messaging - transaction - Purchase tracking -
calendar, day, time_range,
time_slot_entity - Scheduling - Aggregate root tables:
*_aggregate_root (event sourcing pattern) - Domain events:
domain_event table - Join tables: 30+ *_jt
tables for many-to-many relationships
Architecture: - Event sourcing with aggregate roots - Highly normalized with explicit join tables - Contains duplicates of nearly every domain in microservices - Legacy system - DO NOT migrate, extract patterns only
8. Stub/Minimal Databases
peeq-charity-db (2 migrations)
- Stub schema:
-- test projectcomment, no actual tables - Can be deleted or repurposed
peeq-custom-tixr-db (2 migrations)
- Stub schema: Single example table with 3 columns
- Can be deleted or repurposed
Cross-Database Dependency Map
graph TD
subgraph "User Domain"
A[celebrity-db]
B[fan-db]
C[group-profile-db]
end
subgraph "Content Domain"
D[content-db]
E[media-db]
F[shoutout-db]
G[mux-livestream-db]
end
subgraph "Commerce Domain"
H[inventory-db]
I[stripe-db]
J[subscriptions-db]
K[wallet-db]
L[transaction-db]
end
subgraph "Events Domain"
M[broadcast-db]
N[stream-db]
O[onsite-event-db]
P[class-catalog-db]
end
subgraph "Platform Domain"
Q[message-board-db]
R[sse-db]
S[tags-db]
T[notification-service-db]
end
subgraph "Infrastructure"
U[encryption-db]
V[dwolla-db]
W[tracking-db]
end
subgraph "External"
X[Keycloak]
Y[Mux]
Z[Stripe API]
end
%% User dependencies
X --> A
X --> B
A --> S
C --> A
C --> S
%% Content dependencies
F --> H
F --> Y
G --> H
G --> C
G --> Y
%% Commerce dependencies
H --> J
H --> S
H --> A
K --> H
L --> H
I --> Z
%% Event dependencies
M --> H
M --> G
N --> H
O --> C
P --> C
P --> H
P --> A
%% Platform dependencies
Q --> H
R --> H
%% Message Board and SSE similar patterns
Q -.similar to.- R
%% Duplicate schemas
M -.duplicate.- N
D -.subset of.- E
U -.duplicate.- V
style M fill:#ffcccc
style N fill:#ffcccc
style D fill:#ffcccc
style E fill:#ccffcc
style U fill:#ffffcc
style V fill:#ffffcc
Implicit Relationships (No Foreign Keys)
Due to cross-database constraints being impossible in PostgreSQL, these relationships exist only in application code:
| From Database | Field | To Database | Referenced Field |
|---|---|---|---|
| fan-db | celeb_user_id |
celebrity-db | celebrity_user_id |
| inventory-db | associated_subscriptions |
subscriptions-db | id |
| inventory-db | associated_tags |
tags-db | id |
| inventory-db | created_by_id |
celebrity-db | celebrity_user_id |
| shoutout-db | inventory_item_id |
inventory-db | item_id |
| broadcast-db | associated_item_ids |
inventory-db | id |
| class-catalog-db | group_id |
group-profile-db | profile_id |
| wallet-db | item_id |
inventory-db | item_id |
| transaction-db | item_id |
inventory-db | id |
Impact: These relationships cannot be enforced at database level, leading to: - Orphaned records if deletions aren’t coordinated - No cascading updates/deletes - Application-level join logic required for reporting
Domain Groupings
Proposed Consolidation: 5 Core Databases
1. User & Identity
Database (peeq_users)
Consolidates: celebrity-db, fan-db, group-profile-db
Schema Design:
-- Unified profiles with type discriminator
CREATE TABLE profiles (
id UUID PRIMARY KEY,
keycloak_id UUID UNIQUE NOT NULL,
profile_type VARCHAR(50) NOT NULL, -- 'CELEBRITY', 'FAN', 'GROUP'
created_on TIMESTAMP NOT NULL,
meta_data JSONB,
-- Celebrity-specific
bio TEXT,
display_name VARCHAR(255),
status VARCHAR(50),
display_weight INT,
store_code VARCHAR(50),
-- Group-specific
parent_group_id UUID REFERENCES profiles(id),
is_approved BOOLEAN,
owner_id UUID REFERENCES profiles(id),
-- Indexes
CHECK (profile_type IN ('CELEBRITY', 'FAN', 'GROUP'))
);
CREATE TABLE profile_images (
id UUID PRIMARY KEY,
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
image_type VARCHAR(50), -- 'PROFILE', 'BANNER', 'THUMBNAIL'
url TEXT NOT NULL,
created_on TIMESTAMP NOT NULL
);
CREATE TABLE profile_tags (
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
tag_id UUID NOT NULL,
PRIMARY KEY (profile_id, tag_id)
);
CREATE TABLE fan_follows (
id UUID PRIMARY KEY,
fan_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
celebrity_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
followed_on TIMESTAMP NOT NULL,
unfollowed_on TIMESTAMP,
notifications JSONB,
UNIQUE (fan_id, celebrity_id)
);
CREATE TABLE profile_status_history (
id UUID PRIMARY KEY,
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
status VARCHAR(50) NOT NULL,
occurred_on TIMESTAMP NOT NULL,
meta_data JSONB
);
Benefits: - Single source of truth for user data - Foreign key enforcement for follows/relationships - Reduced connection pooling overhead - Simplified authentication integration
Migration Complexity: Medium - Merge 3 schemas with similar structures - Preserve all audit history - Update ~10 dependent services
2. Content & Media
Database (peeq_media)
Consolidates: content-db, media-db, shoutout-db, mux-livestream-db
Schema Design:
-- Unified media files
CREATE TABLE media_files (
id UUID PRIMARY KEY,
owner_id UUID NOT NULL, -- References peeq_users.profiles
media_type VARCHAR(50) NOT NULL, -- 'IMAGE', 'VIDEO', 'AUDIO', 'DOCUMENT'
content_source VARCHAR(50), -- 'UPLOAD', 'MUX_STREAM', 'MUX_LIVESTREAM', 'SHOUTOUT'
-- File metadata
mime_type VARCHAR(255),
content_length BIGINT,
content_md5 VARCHAR(32),
-- Media-specific
duration INT,
width INT,
height INT,
-- Storage
storage_url TEXT,
content_secret VARCHAR(255),
-- State
state VARCHAR(50), -- 'PROCESSING', 'READY', 'FAILED', 'DELETED'
-- Integration JSON
mux_info JSONB,
media_info JSONB,
-- Inventory link (optional)
inventory_item_id UUID, -- References peeq_commerce.inventory_items
-- Audit
created_at TIMESTAMP NOT NULL,
last_modified_on TIMESTAMP NOT NULL,
CHECK (media_type IN ('IMAGE', 'VIDEO', 'AUDIO', 'DOCUMENT'))
);
CREATE TABLE media_streams (
id UUID PRIMARY KEY,
stream_type VARCHAR(50) NOT NULL, -- 'SHOUTOUT', 'LIVESTREAM', 'BROADCAST'
created_by UUID NOT NULL, -- References peeq_users.profiles
name TEXT,
description TEXT,
stream_state VARCHAR(50),
stream_key VARCHAR(255),
-- Mux integration
mux_info JSONB,
-- Inventory
inventory_item_id UUID,
-- Group association
group_id UUID, -- References peeq_users.profiles (type=GROUP)
-- Timestamps
created_on TIMESTAMP NOT NULL,
modified_on TIMESTAMP,
CHECK (stream_type IN ('SHOUTOUT', 'LIVESTREAM', 'BROADCAST'))
);
Benefits: - Unified Mux integration - No more duplicate file metadata - Simplified media pipeline - Foreign keys to user profiles
Migration Complexity: High - 4 databases with overlapping schemas - Large volume of historical media - Must preserve Mux stream associations - Update ~8 dependent services
3. Commerce Database
(peeq_commerce)
Consolidates: inventory-db, stripe-db, subscriptions-db, wallet-db, transaction-db
Schema Design:
-- Unified inventory
CREATE TABLE inventory_items (
id UUID PRIMARY KEY,
item_id VARCHAR(255) UNIQUE NOT NULL,
item_type VARCHAR(50) NOT NULL, -- 'SHOUTOUT', 'CLASS', 'EVENT', 'SUBSCRIPTION', 'CONTENT'
-- Ownership
created_by_id UUID NOT NULL, -- References peeq_users.profiles
-- State
state VARCHAR(50) NOT NULL,
-- Pricing
price INT,
currency VARCHAR(3) DEFAULT 'USD',
-- Inventory
quantity INT,
limit_per_fan INT,
-- Subscriptions
recurring_payment VARCHAR(50),
auto_expires INT,
is_refundable BOOLEAN DEFAULT true,
-- Associations (arrays for backward compat, migrate to join tables)
associated_series UUID[],
associated_subscriptions UUID[],
associated_tags UUID[],
keywords VARCHAR[],
restricted_users UUID[],
-- Metadata
metadata JSONB,
created_on TIMESTAMP NOT NULL,
CHECK (item_type IN ('SHOUTOUT', 'CLASS', 'EVENT', 'SUBSCRIPTION', 'CONTENT', 'PRODUCT'))
);
-- Subscription plans
CREATE TABLE subscription_plans (
id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
recurring_payment VARCHAR(50) NOT NULL, -- 'MONTHLY', 'YEARLY'
auto_expires INT,
created_by_id UUID NOT NULL,
created_on TIMESTAMP NOT NULL
);
-- Series (content bundles)
CREATE TABLE content_series (
id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
created_by_id UUID NOT NULL,
created_on TIMESTAMP NOT NULL
);
-- User wallets
CREATE TABLE user_wallets (
id BIGSERIAL PRIMARY KEY,
user_id UUID UNIQUE NOT NULL, -- References peeq_users.profiles
balance INT DEFAULT 0,
currency VARCHAR(3) DEFAULT 'USD',
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Transactions (unified)
CREATE TABLE transactions (
id UUID PRIMARY KEY,
transaction_type VARCHAR(50) NOT NULL, -- 'PURCHASE', 'REFUND', 'CREDIT', 'DEBIT'
-- Parties
user_id UUID NOT NULL, -- References peeq_users.profiles
created_by_id UUID NOT NULL,
-- Purchase details
item_id UUID, -- References inventory_items
purchase_id UUID,
-- Financial
amount INT NOT NULL,
currency VARCHAR(3) DEFAULT 'USD',
is_debit BOOLEAN,
-- State
status VARCHAR(50) DEFAULT 'PENDING',
-- Metadata
description TEXT,
meta_data JSONB,
-- Audit
created_on TIMESTAMP NOT NULL,
CHECK (transaction_type IN ('PURCHASE', 'REFUND', 'CREDIT', 'DEBIT', 'TRANSFER'))
);
CREATE TABLE transaction_relations (
transaction_id UUID NOT NULL REFERENCES transactions(id) ON DELETE CASCADE,
related_transaction_id UUID NOT NULL REFERENCES transactions(id),
relation_type VARCHAR(50), -- 'REFUND_OF', 'SPLIT_FROM'
PRIMARY KEY (transaction_id, related_transaction_id)
);
-- Exchange rates
CREATE TABLE exchange_rates (
id UUID PRIMARY KEY,
name VARCHAR(255),
from_currency VARCHAR(3),
to_currency VARCHAR(3),
price INT NOT NULL,
amount INT NOT NULL,
keywords TEXT[],
meta_data JSONB,
created_on TIMESTAMP NOT NULL,
deleted_on TIMESTAMP
);
-- History tables
CREATE TABLE inventory_price_history (
id UUID PRIMARY KEY,
inventory_item_id UUID NOT NULL REFERENCES inventory_items(id) ON DELETE CASCADE,
old_value INT NOT NULL,
new_value INT NOT NULL,
changed_on TIMESTAMP NOT NULL
);
CREATE TABLE inventory_quantity_history (
id UUID PRIMARY KEY,
inventory_item_id UUID NOT NULL REFERENCES inventory_items(id) ON DELETE CASCADE,
old_value INT NOT NULL,
new_value INT NOT NULL,
changed_on TIMESTAMP NOT NULL
);
CREATE TABLE inventory_state_history (
id UUID PRIMARY KEY,
inventory_item_id UUID NOT NULL REFERENCES inventory_items(id) ON DELETE CASCADE,
old_value VARCHAR(50) NOT NULL,
new_value VARCHAR(50) NOT NULL,
changed_on TIMESTAMP NOT NULL
);
Benefits: - Consistent transaction tracking - Atomic balance updates with transactions - Foreign key integrity for purchases - Single source for pricing/inventory
Migration Complexity: Very High - 5 databases with critical financial data - Must maintain transaction history integrity - Requires careful balance reconciliation - Stripe integration must remain stable - Update ~15 dependent services
4. Events & Streaming
Database (peeq_events)
Consolidates: broadcast-db, stream-db, onsite-event-db, class-catalog-db
Schema Design:
-- Unified events (all types)
CREATE TABLE events (
id UUID PRIMARY KEY,
event_type VARCHAR(50) NOT NULL, -- 'BROADCAST', 'STREAM', 'ONSITE', 'CLASS_SESSION'
-- Polymorphic ID (legacy pattern)
id_str VARCHAR(255),
id_type VARCHAR(50),
-- Basic info
name VARCHAR(255),
description TEXT,
-- Creator
created_by_id UUID NOT NULL, -- References peeq_users.profiles
created_on TIMESTAMP NOT NULL,
-- Scheduling
starts_on TIMESTAMP,
stops_on TIMESTAMP,
-- For onsite events
event_date TIMESTAMP,
event_sign_up_type VARCHAR(50),
-- Group/organization
group_id UUID, -- References peeq_users.profiles (type=GROUP)
-- Inventory
inventory_state VARCHAR(50),
inventory_info JSONB,
associated_item_ids UUID[], -- References peeq_commerce.inventory_items
-- Integration
mux_info JSONB,
meta_data JSONB,
CHECK (event_type IN ('BROADCAST', 'STREAM', 'ONSITE', 'CLASS_SESSION'))
);
CREATE INDEX idx_events_type ON events(event_type);
CREATE INDEX idx_events_date ON events(event_date);
CREATE INDEX idx_events_starts ON events(starts_on);
CREATE INDEX idx_events_group ON events(group_id);
-- Event check-ins (onsite events)
CREATE TABLE event_checkins (
id UUID PRIMARY KEY,
event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE,
user_id UUID NOT NULL, -- References peeq_users.profiles
checked_in_at TIMESTAMP NOT NULL
);
-- Classes (educational content)
CREATE TABLE classes (
id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
prerequisites TEXT,
blog_id UUID,
group_id UUID, -- References peeq_users.profiles (type=GROUP)
created_by_id UUID NOT NULL,
created_on TIMESTAMP NOT NULL
);
CREATE TABLE class_thumbnails (
class_id UUID NOT NULL REFERENCES classes(id) ON DELETE CASCADE,
thumbnail_url TEXT,
PRIMARY KEY (class_id, thumbnail_url)
);
CREATE TABLE class_details (
id UUID PRIMARY KEY,
class_id UUID NOT NULL REFERENCES classes(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
display_order INT NOT NULL
);
CREATE TABLE class_tags (
class_id UUID NOT NULL REFERENCES classes(id) ON DELETE CASCADE,
tag_id UUID NOT NULL,
PRIMARY KEY (class_id, tag_id)
);
CREATE TABLE class_badges (
class_id UUID NOT NULL REFERENCES classes(id) ON DELETE CASCADE,
badge_id VARCHAR(255),
PRIMARY KEY (class_id, badge_id)
);
CREATE TABLE class_sessions (
id UUID PRIMARY KEY,
class_id UUID REFERENCES classes(id) ON DELETE CASCADE,
attendance_format VARCHAR(50), -- 'ONLINE', 'ONSITE', 'HYBRID'
inventory_info JSONB,
original_price INT
);
CREATE TABLE session_instructors (
session_id UUID NOT NULL REFERENCES class_sessions(id) ON DELETE CASCADE,
instructor_id UUID NOT NULL, -- References peeq_users.profiles
PRIMARY KEY (session_id, instructor_id)
);
Benefits: - Eliminate broadcast/stream duplication - Unified event scheduling - Consistent inventory associations - Simplified analytics (all events in one place)
Migration Complexity: Medium-High - 4 databases, 2 with identical schemas - Large volume of historical events - Class catalog has complex hierarchical structure - Update ~6 dependent services
5. Platform Services
Database (peeq_platform)
Consolidates: message-board-db, sse-db, tags-db, notification-service-db
Schema Design:
-- Tags (centralized taxonomy)
CREATE TABLE tags (
id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
tag_type VARCHAR(50) NOT NULL, -- 'CATEGORY', 'SKILL', 'SPORT', 'TOPIC'
color VARCHAR(7), -- Hex color
thumbnail VARCHAR(255),
keywords TEXT[],
associated_tags UUID[],
meta_data JSONB,
created_on TIMESTAMP NOT NULL,
created_by_id UUID,
last_modified_on TIMESTAMP,
updated_by_id UUID
);
CREATE INDEX idx_tags_type ON tags(tag_type);
CREATE INDEX idx_tags_name ON tags(name);
-- Real-time groups (SSE, message boards)
CREATE TABLE realtime_groups (
id UUID PRIMARY KEY,
group_type VARCHAR(50) NOT NULL, -- 'SSE', 'MESSAGE_BOARD', 'CHAT'
-- Polymorphic ID
id_str VARCHAR(255),
id_type VARCHAR(50),
-- Basic info
name VARCHAR(255),
description TEXT,
-- Creator
created_by_id UUID NOT NULL,
created_on TIMESTAMP NOT NULL,
-- Lifecycle
starts_on TIMESTAMP,
stops_on TIMESTAMP,
-- Inventory
associated_item_ids UUID[],
-- Metadata
meta_data JSONB,
CHECK (group_type IN ('SSE', 'MESSAGE_BOARD', 'CHAT'))
);
CREATE TABLE group_members (
id UUID PRIMARY KEY,
group_id UUID NOT NULL REFERENCES realtime_groups(id) ON DELETE CASCADE,
user_id UUID NOT NULL, -- References peeq_users.profiles
name VARCHAR(255),
added_on TIMESTAMP NOT NULL,
removed_on TIMESTAMP,
meta_data JSONB,
UNIQUE (group_id, user_id, removed_on)
);
CREATE TABLE group_messages (
id UUID PRIMARY KEY,
group_id UUID NOT NULL REFERENCES realtime_groups(id) ON DELETE CASCADE,
member_id UUID NOT NULL REFERENCES group_members(id),
message_content TEXT NOT NULL,
message_content_type VARCHAR(50), -- 'TEXT', 'IMAGE', 'VIDEO', 'RICH'
meta_data JSONB,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Notifications
CREATE TABLE notification_templates (
id UUID PRIMARY KEY,
template_key VARCHAR(255) UNIQUE NOT NULL,
channel VARCHAR(50) NOT NULL, -- 'EMAIL', 'SMS', 'PUSH', 'IN_APP'
subject TEXT,
body_template TEXT NOT NULL,
created_on TIMESTAMP NOT NULL
);
CREATE TABLE notifications (
id UUID PRIMARY KEY,
user_id UUID NOT NULL, -- References peeq_users.profiles
template_key VARCHAR(255) REFERENCES notification_templates(template_key),
channel VARCHAR(50) NOT NULL,
-- Content (rendered from template)
subject TEXT,
body TEXT NOT NULL,
-- State
status VARCHAR(50) DEFAULT 'PENDING', -- 'PENDING', 'SENT', 'FAILED', 'READ'
-- Metadata
meta_data JSONB,
-- Audit
created_on TIMESTAMP NOT NULL,
sent_on TIMESTAMP,
read_on TIMESTAMP,
CHECK (channel IN ('EMAIL', 'SMS', 'PUSH', 'IN_APP')),
CHECK (status IN ('PENDING', 'SENT', 'FAILED', 'READ'))
);
CREATE INDEX idx_notifications_user ON notifications(user_id, status);
CREATE INDEX idx_notifications_status ON notifications(status, created_on);
Benefits: - Eliminate SSE/message-board duplication - Centralized tag management - Unified notification tracking - Reduced connection overhead
Migration Complexity: Medium - 4 databases with minimal overlap - Tags database referenced by many services (requires coordination) - Update ~12 dependent services
Databases to Retain Separately
1.
Infrastructure: peeq_encryption (merged from encryption-db
+ dwolla-db)
Rationale: - Security boundary isolation - Encryption key management best practice - Minimize attack surface
Consolidation: - Merge encryption-db and dwolla-db (identical schemas) - Use namespace separation for Dwolla vs general secrets
Schema:
-- Keep existing vault_namespaces and vault_secrets schema
-- Add namespace_type column for separation
ALTER TABLE vault_namespaces ADD COLUMN namespace_type VARCHAR(50) DEFAULT 'GENERAL';
-- 'GENERAL', 'DWOLLA', 'STRIPE', 'MUX'
2. Analytics:
node-tracking-db (MySQL)
Rationale: - Different database technology (MySQL vs PostgreSQL) - Analytics workload (OLAP vs OLTP) - UTM tracking and link management - Could migrate to PostgreSQL but low ROI
Keep as-is unless migrating to dedicated analytics platform (e.g., BigQuery, Snowflake)
3. Legacy:
peeq_flyway (read-only archive)
Rationale: - Legacy monolith schema - Event sourcing architecture - Historical data only - Do NOT migrate or consolidate
Action: - Mark as read-only - Preserve for data archaeology - Extract patterns for new architecture, not data
Consolidation Recommendations
Phase 1: Quick Wins (Low Risk, High Impact)
Target: Eliminate obvious duplicates
| Action | Databases | Effort | Impact |
|---|---|---|---|
| Merge broadcast-db → stream-db | 2 → 1 | 1 week | Identical schemas |
| Merge content-db → media-db | 2 → 1 | 1 week | Subset merge |
| Merge encryption-db + dwolla-db | 2 → 1 | 3 days | Identical schemas |
Estimated Reduction: 5 databases → 2 databases Services to Update: ~6 services Timeline: 2-3 weeks
Phase 2: Domain Consolidation (Medium Risk, High Value)
Target: Create domain-aligned databases
| Domain | Source Databases | Target | Effort | Impact |
|---|---|---|---|---|
| User & Identity | celebrity-db, fan-db, group-profile-db | peeq_users | 6 weeks | Foreign key enforcement |
| Content & Media | content-db, media-db, shoutout-db, mux-livestream-db | peeq_media | 8 weeks | Unified Mux integration |
| Events & Streaming | broadcast-db, stream-db, onsite-event-db, class-catalog-db | peeq_events | 6 weeks | Eliminate duplication |
| Platform Services | message-board-db, sse-db, tags-db, notification-service-db | peeq_platform | 4 weeks | Centralized tags |
Estimated Reduction: 15 databases → 4 databases Services to Update: ~25 services Timeline: 4-6 months
Phase 3: Commerce Consolidation (High Risk, Critical)
Target: Unified commerce database
| Domain | Source Databases | Target | Effort | Impact |
|---|---|---|---|---|
| Commerce | inventory-db, stripe-db, subscriptions-db, wallet-db, transaction-db | peeq_commerce | 12 weeks | ACID transactions |
Complexity: - Financial data requires careful migration - Must maintain transaction history integrity - Zero-downtime migration required - Balance reconciliation critical
Estimated Reduction: 5 databases → 1 database Services to Update: ~15 services Timeline: 3-4 months (dedicated focus)
Final State: 8 Databases (from 26)
| Database | Purpose | Source Count | Technology |
|---|---|---|---|
peeq_users |
User profiles, follows, groups | 3 | PostgreSQL |
peeq_media |
Files, streams, Mux integration | 4 | PostgreSQL |
peeq_commerce |
Inventory, transactions, wallets | 5 | PostgreSQL |
peeq_events |
Broadcasts, classes, onsite events | 4 | PostgreSQL |
peeq_platform |
Tags, messaging, notifications | 4 | PostgreSQL |
peeq_encryption |
Vault, secrets, keys | 2 | PostgreSQL |
node_tracking |
Analytics, UTM, link tracking | 1 | MySQL |
peeq_flyway |
Legacy archive (read-only) | 1 | PostgreSQL |
Total Reduction: 26 → 8 databases (69% reduction)
Migration Risk Assessment
Risk Categories
| Risk | Phase 1 | Phase 2 | Phase 3 | Mitigation |
|---|---|---|---|---|
| Data Loss | Low | Medium | High | Comprehensive backup, dry-run migrations, rollback plan |
| Downtime | Low | Medium | High | Blue-green deployment, dual-write strategy |
| Data Inconsistency | Low | Medium | Very High | Transaction scripts, reconciliation checks |
| Service Disruption | Low | Medium | High | Feature flags, gradual rollout |
| Financial Impact | None | Low | Critical | Dedicated team, extended testing, manual verification |
Critical Success Factors
- Automated Testing
- Unit tests for all data access layers
- Integration tests for cross-service transactions
- Load tests for consolidated databases
- Migration Tooling
- Idempotent migration scripts
- Data validation after each migration
- Automated rollback capability
- Dual-Write Strategy (for Phase 2 & 3)
- Write to both old and new databases during transition
- Compare results for consistency
- Gradual cutover to new database
- Monitoring
- Transaction volume tracking
- Balance reconciliation dashboards
- Error rate alerting
- Performance regression detection
- Rollback Plan
- Database snapshots before each migration
- Service version pinning
- Quick rollback procedures documented
Implementation Strategy
Approach: Strangler Fig Pattern
- Create new consolidated database
- Dual-write to both old and new
- Migrate historical data
- Validate data consistency
- Switch reads to new database
- Stop writes to old database
- Decommission old database
Example: User Domain Consolidation
-- Step 1: Create peeq_users with unified schema
CREATE DATABASE peeq_users;
-- Step 2: Migrate celebrity profiles
INSERT INTO peeq_users.profiles (id, keycloak_id, profile_type, bio, display_name, ...)
SELECT celebrity_user_id, celebrity_user_id, 'CELEBRITY', bio, display_name, ...
FROM peeq_celebrity.celebrity_profiles;
-- Step 3: Migrate fan profiles
INSERT INTO peeq_users.profiles (id, keycloak_id, profile_type, created_on, meta_data)
SELECT fan_user_id, fan_user_id, 'FAN', created_on, meta_data
FROM peeq_fan.fan_profiles;
-- Step 4: Migrate follows (with new foreign keys)
INSERT INTO peeq_users.fan_follows (id, fan_id, celebrity_id, followed_on, unfollowed_on, notifications)
SELECT id, fan_user_id, celeb_user_id, followed_on, unfollowed_on, notifications
FROM peeq_fan.fan_follows
WHERE unfollowed_on IS NULL; -- Active follows only
-- Step 5: Validate counts
SELECT 'CELEBRITY', COUNT(*) FROM peeq_users.profiles WHERE profile_type = 'CELEBRITY'
UNION ALL
SELECT 'FAN', COUNT(*) FROM peeq_users.profiles WHERE profile_type = 'FAN'
UNION ALL
SELECT 'FOLLOWS', COUNT(*) FROM peeq_users.fan_follows;
-- Compare with source databases
Service Update Pattern
// Old: Multiple database connections
@Autowired private CelebrityRepository celebrityRepo;
@Autowired private FanRepository fanRepo;
// New: Unified user service
@Autowired private UserProfileRepository userRepo;
// Transition: Dual-write during migration
public void updateCelebrity(UUID id, CelebrityDTO dto) {
// Write to old database
celebrityRepo.save(dto);
// Write to new database (if feature flag enabled)
if (featureFlags.isEnabled("USE_UNIFIED_USERS")) {
userRepo.save(dto.toProfile());
}
}
// After validation: Switch to new database only
public void updateProfile(UUID id, ProfileDTO dto) {
userRepo.save(dto);
}
Performance Implications
Connection Pool Optimization
Current State: - 26 databases × 10 connections/service × 20 services = 5,200 potential connections - Actual: ~500-800 active connections across cluster
Consolidated State: - 8 databases × 20 connections/service × 20 services = 3,200 potential connections - Estimated: ~300-400 active connections
Savings: - 40-50% reduction in connection overhead - Reduced PostgreSQL server memory (each connection ~10MB) - Faster connection acquisition
Query Performance
Improvements: - Foreign key joins replace application-level joins (10-100x faster) - Single transaction for multi-table operations (ACID guarantees) - Reduced network hops for related data
Example:
-- Current: 2 queries across databases (application join)
-- Query 1 (peeq_fan):
SELECT celeb_user_id FROM fan_follows WHERE fan_user_id = ?;
-- Query 2 (peeq_celebrity):
SELECT * FROM celebrity_profiles WHERE celebrity_user_id IN (?);
-- Consolidated: 1 query with join
SELECT p.*
FROM fan_follows ff
JOIN profiles p ON p.id = ff.celebrity_id
WHERE ff.fan_id = ?;
Trade-offs: - Larger databases (more rows per table) - Must manage connection pools per domain - Potential for lock contention (mitigate with row-level locking)
Data Volume Estimates
| Database | Estimated Rows | Size (GB) | Post-Consolidation | Notes |
|---|---|---|---|---|
| celebrity-db | 5,000 profiles | 0.5 | → peeq_users | Includes images, tags, history |
| fan-db | 100,000 profiles | 2.0 | → peeq_users | Includes follows |
| group-profile-db | 500 groups | 0.1 | → peeq_users | |
| peeq_users total | ~105,500 | 2.6 GB | ||
| media-db | 500,000 files | 5.0 | → peeq_media | Metadata only |
| shoutout-db | 50,000 streams | 1.0 | → peeq_media | |
| mux-livestream-db | 10,000 streams | 0.5 | → peeq_media | |
| peeq_media total | ~560,000 | 6.5 GB | ||
| inventory-db | 100,000 items | 3.0 | → peeq_commerce | Includes history |
| wallet-db | 100,000 wallets | 1.0 | → peeq_commerce | |
| transaction-db | 2,000,000 txns | 10.0 | → peeq_commerce | Largest table |
| stripe-db | 10,000 rates | 0.1 | → peeq_commerce | |
| subscriptions-db | 5,000 plans | 0.2 | → peeq_commerce | |
| peeq_commerce total | ~2,215,000 | 14.3 GB | Critical for performance |
Total Current: ~26 GB across 26 databases Total Consolidated: ~26 GB across 8 databases (same data, better organized)
Conclusion
This analysis identifies a clear path to reduce database complexity from 26 to 8 databases through domain-driven consolidation. The proposed three-phase approach balances risk with value delivery:
- Phase 1 (Quick Wins): Eliminate obvious duplicates in 2-3 weeks
- Phase 2 (Domain Consolidation): Create bounded context databases in 4-6 months
- Phase 3 (Commerce Consolidation): Unify financial data in 3-4 months
The consolidation will deliver: - 69% reduction in database count - Foreign key integrity replacing application-level consistency - 40-50% reduction in connection pool overhead - Simplified operations with fewer backup/restore cycles - Improved query performance through joins instead of application-level aggregation
The highest risk area is commerce consolidation (Phase 3) due to financial data sensitivity. This phase requires dedicated focus, extensive testing, and a robust rollback plan.
Recommendation: Proceed with Phase 1 immediately, begin planning Phase 2 in parallel, and defer Phase 3 until Phase 2 stabilizes.
Appendices
Appendix A: Database Technology Stack
- RDBMS: PostgreSQL 13+ (25 databases), MySQL 8.0 (1 database)
- Migration Tool: Flyway (all databases use Flyway versioned migrations)
- Connection Pooling: HikariCP (Java services)
- ORM: Spring Data JPA / Hibernate (Java services)
Appendix B: Foreign Key Constraint Examples
-- Example: Fan follows with referential integrity
ALTER TABLE fan_follows
ADD CONSTRAINT fk_fan_follows_fan
FOREIGN KEY (fan_id) REFERENCES profiles(id) ON DELETE CASCADE;
ALTER TABLE fan_follows
ADD CONSTRAINT fk_fan_follows_celebrity
FOREIGN KEY (celebrity_id) REFERENCES profiles(id) ON DELETE CASCADE;
-- Example: Transactions with inventory
ALTER TABLE transactions
ADD CONSTRAINT fk_transactions_item
FOREIGN KEY (item_id) REFERENCES inventory_items(id) ON DELETE SET NULL;
ALTER TABLE transactions
ADD CONSTRAINT fk_transactions_user
FOREIGN KEY (user_id) REFERENCES profiles(id) ON DELETE RESTRICT;
Appendix C: Service Dependency Matrix
| Service | Current DB Dependencies | Post-Consolidation | Change Impact |
|---|---|---|---|
| celebrity-service | celebrity-db, tags-db | peeq_users, peeq_platform | Medium |
| fan-service | fan-db, celebrity-db | peeq_users | Low |
| inventory-service | inventory-db, stripe-db, subscriptions-db | peeq_commerce | High |
| shoutout-service | shoutout-db, inventory-db, media-db | peeq_media, peeq_commerce | Medium |
| broadcast-service | broadcast-db, inventory-db, mux-livestream-db | peeq_events, peeq_commerce, peeq_media | High |
(Full matrix available in separate service inventory document)
Appendix D: Rollback Checklist
Document Prepared By: Claude (Database Analysis Agent) Review Status: Pending Human Review Next Steps: Present to architecture team for consolidation roadmap approval