Architecture

Database Consolidation Analysis

Last updated: 2026-02-01 | Architecture

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

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)

peeq-custom-tixr-db (2 migrations)


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

  1. Automated Testing
    • Unit tests for all data access layers
    • Integration tests for cross-service transactions
    • Load tests for consolidated databases
  2. Migration Tooling
    • Idempotent migration scripts
    • Data validation after each migration
    • Automated rollback capability
  3. 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
  4. Monitoring
    • Transaction volume tracking
    • Balance reconciliation dashboards
    • Error rate alerting
    • Performance regression detection
  5. Rollback Plan
    • Database snapshots before each migration
    • Service version pinning
    • Quick rollback procedures documented

Implementation Strategy

Approach: Strangler Fig Pattern

  1. Create new consolidated database
  2. Dual-write to both old and new
  3. Migrate historical data
  4. Validate data consistency
  5. Switch reads to new database
  6. Stop writes to old database
  7. 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:

  1. Phase 1 (Quick Wins): Eliminate obvious duplicates in 2-3 weeks
  2. Phase 2 (Domain Consolidation): Create bounded context databases in 4-6 months
  3. 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

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