nordabiz/docs/architecture/05-database-schema.md
Maciej Pienczyn cebe52f303 refactor: Rebranding i aktualizacja modelu AI
- Zmiana nazwy: "Norda Biznes Hub" → "Norda Biznes Partner"
- Aktualizacja modelu AI: Gemini 2.0 Flash → Gemini 3 Flash
- Zachowano historyczne odniesienia w timeline i dokumentacji

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 14:08:39 +01:00

37 KiB

Database Schema Diagram (Entity Relationship Diagram)

Document Version: 1.0 Last Updated: 2026-01-10 Status: Production LIVE Diagram Type: Entity Relationship Diagram (ERD)


Overview

This diagram shows the complete database schema for the Norda Biznes Partner application. It illustrates:

  • All 36 database entities (tables) organized into functional domains
  • Relationships between entities with proper cardinality
  • Key constraints (primary keys, foreign keys, unique constraints)
  • Data organization patterns and domain boundaries

Database Technology: PostgreSQL 14+ ORM: SQLAlchemy 2.0 Total Tables: 36 Total Relationships: 60+ foreign key relationships Special Features: Full-Text Search (FTS), JSONB, ARRAY types, fuzzy matching

Abstraction Level: Data Model (ERD) Audience: Database Administrators, Backend Developers, System Architects Purpose: Understanding data structure, relationships, and database design patterns


Database Architecture Overview

Functional Domains

Domain Tables Purpose
User Management 1 User accounts, authentication, authorization
Company Directory 10 Company data, services, competencies, certifications
Digital Maturity 5 Digital maturity scoring, website analysis, quality tracking
AI Chat 4 Chat conversations, messages, feedback, cost tracking
Forum 2 Community forum topics and replies
Calendar/Events 2 Norda Biznes events and RSVPs
Private Messages 1 Peer-to-peer messaging
B2B Classifieds 1 Company listings (Szukam/Oferuję)
Social & Contact 3 Social media profiles, contact info, recommendations
Auditing Systems 3 GBP audits, IT audits, collaboration matching
Membership Fees 2 Payment tracking, fee configuration
Notifications 1 In-app notifications

Complete Entity Relationship Diagram

erDiagram
    %% ============================================================
    %% CORE DOMAIN - User Management & Company Directory
    %% ============================================================

    users {
        int id PK
        string email UK "UNIQUE, indexed"
        string password_hash "NOT NULL"
        string name
        int company_id FK "nullable"
        string company_nip
        boolean is_active "default: true"
        boolean is_verified "default: false"
        boolean is_admin "default: false"
        boolean is_norda_member "default: false"
        timestamp created_at
        timestamp last_login
        string verification_token
        string reset_token
    }

    companies {
        int id PK
        string name "NOT NULL"
        string legal_name
        string slug UK "UNIQUE, indexed"
        int category_id FK
        string nip UK "UNIQUE, 10 digits"
        string regon "9 or 14 digits"
        string krs "10 digits"
        string website
        string email
        string phone
        string status "active/inactive"
        string data_quality "basic/enhanced/complete"
        int digital_maturity_score "0-100"
        boolean ai_enabled
        array ai_tools_used "PostgreSQL ARRAY"
        timestamp created_at
    }

    categories {
        int id PK
        string name UK "UNIQUE"
        string slug UK "UNIQUE"
        text description
        string icon
        int sort_order
    }

    services {
        int id PK
        string name UK "UNIQUE"
        string slug UK "UNIQUE"
        text description
    }

    competencies {
        int id PK
        string name UK "UNIQUE"
        string slug UK "UNIQUE"
        string category
        text description
    }

    company_services {
        int company_id PK,FK
        int service_id PK,FK
        boolean is_primary "default: false"
        timestamp added_at
    }

    company_competencies {
        int company_id PK,FK
        int competency_id PK,FK
        string level "proficiency level"
        timestamp added_at
    }

    certifications {
        int id PK
        int company_id FK
        string name "NOT NULL"
        string issuer
        string certificate_number
        date issue_date
        date expiry_date
        boolean is_active
    }

    awards {
        int id PK
        int company_id FK
        string name "NOT NULL"
        string issuer
        int year
        text description
    }

    company_events {
        int id PK
        int company_id FK
        string event_type "news_mention, press_release, etc"
        string title "NOT NULL"
        text description
        date event_date
        string source_url
    }

    %% ============================================================
    %% DIGITAL MATURITY DOMAIN
    %% ============================================================

    company_digital_maturity {
        int id PK
        int company_id UK "UNIQUE"
        int overall_score "0-100"
        int online_presence_score "0-100"
        int social_media_score "0-100"
        int it_infrastructure_score "0-100"
        int business_applications_score "0-100"
        int backup_disaster_recovery_score "0-100"
        int cybersecurity_score "0-100"
        int ai_readiness_score "0-100"
        int digital_marketing_score "0-100"
        array critical_gaps
        string improvement_priority "critical/high/medium/low"
        numeric estimated_investment_needed
        int rank_in_category
        int rank_overall
        int percentile
        numeric total_opportunity_value
        string sales_readiness "hot/warm/cold/not_ready"
    }

    company_website_analysis {
        int id PK
        int company_id FK
        string website_url
        int http_status_code
        int load_time_ms
        boolean has_ssl
        timestamp ssl_expires_at
        boolean is_responsive
        string cms_detected
        numeric google_rating "0.0-5.0"
        int google_reviews_count
        string google_place_id
        int content_richness_score "1-10"
        int pagespeed_seo_score "0-100"
        int pagespeed_performance_score "0-100"
        int pagespeed_accessibility_score "0-100"
        int pagespeed_best_practices_score "0-100"
        jsonb pagespeed_audits
        boolean has_google_analytics
        int opportunity_score "0-100"
        numeric estimated_project_value
        timestamp analyzed_at
    }

    maturity_assessments {
        int id PK
        int company_id FK
        int assessed_by_user_id FK
        timestamp assessed_at
        string assessment_type "full/quick/self_reported/audit"
        int overall_score
        int online_presence_score
        int social_media_score
        int it_infrastructure_score
        int score_change "change since last"
        array areas_improved
        array areas_declined
        text notes
    }

    company_quality_tracking {
        int id PK
        int company_id UK "UNIQUE"
        int verification_count
        timestamp last_verified_at
        string verified_by
        text verification_notes
        int quality_score "0-100"
        int issues_found
        int issues_fixed
    }

    company_website_content {
        int id PK
        int company_id FK
        timestamp scraped_at
        string url
        int http_status
        text raw_html
        text raw_text
        string page_title
        text meta_description
        text main_content
        array email_addresses
        array phone_numbers
        jsonb social_media
        int word_count
    }

    company_ai_insights {
        int id PK
        int company_id UK "UNIQUE"
        int content_id FK
        text business_summary
        array services_list
        text target_market
        array unique_selling_points
        array company_values
        array certifications
        string suggested_category
        numeric category_confidence "0.00-1.00"
        array industry_tags
        numeric ai_confidence_score
        int processing_time_ms
    }

    %% ============================================================
    %% AI CHAT DOMAIN
    %% ============================================================

    ai_chat_conversations {
        int id PK
        int user_id FK "indexed"
        string title
        string conversation_type "general/search"
        timestamp started_at
        timestamp updated_at
        boolean is_active
        int message_count
        string model_name
    }

    ai_chat_messages {
        int id PK
        int conversation_id FK "indexed"
        timestamp created_at
        string role "user/assistant"
        text content "NOT NULL"
        int tokens_input
        int tokens_output
        numeric cost_usd
        int latency_ms
        boolean edited
        boolean regenerated
        int feedback_rating "1=down, 2=up"
        text feedback_comment
        int companies_mentioned
        string query_intent
    }

    ai_chat_feedback {
        int id PK
        int message_id UK "UNIQUE"
        int user_id FK
        int rating "1-5 stars"
        boolean is_helpful
        boolean is_accurate
        boolean found_company
        text comment
        text suggested_answer
        text original_query
        text expected_companies
        timestamp created_at
    }

    ai_api_costs {
        int id PK
        timestamp timestamp "indexed"
        string api_provider "gemini/brave/etc"
        string model_name
        string feature "ai_chat/general"
        int user_id FK "indexed"
        int input_tokens
        int output_tokens
        int total_tokens
        numeric input_cost "USD"
        numeric output_cost "USD"
        numeric total_cost "USD"
        boolean success
        text error_message
        int latency_ms
        string prompt_hash "SHA256"
    }

    %% ============================================================
    %% COMMUNITY FEATURES DOMAIN
    %% ============================================================

    forum_topics {
        int id PK
        string title "NOT NULL"
        text content "NOT NULL"
        int author_id FK
        boolean is_pinned
        boolean is_locked
        int views_count
        timestamp created_at
        timestamp updated_at
    }

    forum_replies {
        int id PK
        int topic_id FK
        int author_id FK
        text content "NOT NULL"
        timestamp created_at
        timestamp updated_at
    }

    norda_events {
        int id PK
        string title "NOT NULL"
        text description
        string event_type "meeting/webinar/networking"
        date event_date "NOT NULL"
        time time_start
        time time_end
        string location
        string location_url
        string speaker_name
        int speaker_company_id FK
        boolean is_featured
        int max_attendees
        int created_by FK
        timestamp created_at
    }

    event_attendees {
        int id PK
        int event_id FK
        int user_id FK
        string status "confirmed/maybe/declined"
        timestamp registered_at
    }

    private_messages {
        int id PK
        int sender_id FK
        int recipient_id FK
        string subject
        text content "NOT NULL"
        boolean is_read
        timestamp read_at
        int parent_id FK "thread support"
        timestamp created_at
    }

    classifieds {
        int id PK
        int author_id FK
        int company_id FK
        string listing_type "szukam/oferuje"
        string category "uslugi/produkty/wspolpraca"
        string title "NOT NULL"
        text description "NOT NULL"
        string budget_info
        string location_info
        boolean is_active
        timestamp expires_at
        int views_count
        timestamp created_at
    }

    %% ============================================================
    %% SOCIAL & CONTACT DOMAIN
    %% ============================================================

    company_contacts {
        int id PK
        int company_id FK
        string contact_type "phone/email/fax/mobile"
        string value
        string purpose "Biuro/Sprzedaż"
        boolean is_primary
        string source "website/krs/google"
        string source_url
        date source_date
        boolean is_verified
        timestamp verified_at
        string verified_by
    }

    company_social_media {
        int id PK
        int company_id FK "indexed"
        string platform "facebook/linkedin/instagram"
        string url
        timestamp verified_at "indexed"
        string source "website_scrape/brave_search"
        boolean is_valid
        timestamp last_checked_at
        string check_status "ok/404/redirect"
        string page_name
        int followers_count
        timestamp created_at
    }

    company_recommendations {
        int id PK
        int company_id FK "indexed"
        int user_id FK "indexed"
        text recommendation_text
        string service_category
        boolean show_contact
        string status "pending/approved/rejected"
        int moderated_by FK
        timestamp moderated_at
        text rejection_reason
        timestamp created_at
    }

    %% ============================================================
    %% AUDITING SYSTEMS DOMAIN
    %% ============================================================

    gbp_audits {
        int id PK
        int company_id FK "indexed"
        timestamp audit_date "indexed"
        int completeness_score "0-100"
        jsonb fields_status
        jsonb recommendations
        boolean has_name
        boolean has_address
        boolean has_phone
        boolean has_website
        boolean has_hours
        boolean has_categories
        boolean has_photos
        boolean has_description
        boolean has_services
        boolean has_reviews
        int photo_count
        boolean logo_present
        boolean cover_photo_present
        int review_count
        numeric average_rating "0.0-5.0"
        string google_place_id
        string google_maps_url
        string audit_source "manual/automated/api"
        string audit_version
        text audit_errors
    }

    it_audits {
        int id PK
        int company_id FK "indexed"
        timestamp audit_date "indexed"
        string audit_source "form/api_sync"
        int audited_by FK
        int overall_score "0-100"
        int completeness_score "0-100"
        int security_score "0-100"
        int collaboration_score "0-100"
        string maturity_level "basic/developing/established/advanced"
        boolean has_it_manager
        boolean it_outsourced
        string it_provider_name
        boolean has_azure_ad
        string azure_tenant_name
        string azure_user_count "1-10, 11-50, etc"
        boolean has_m365
        array m365_plans
        array teams_usage
        boolean has_google_workspace
        string server_count "0, 1-3, 4-10, 10+"
        array server_types "physical/vm_onprem/cloud_iaas"
        string virtualization_platform "vmware/hyperv/proxmox"
        array server_os
        string network_firewall_brand
        string employee_count
        string computer_count
        array desktop_os
        boolean has_mdm
        string antivirus_solution
        boolean has_edr
        boolean has_vpn
        boolean has_mfa
        array mfa_scope
        string backup_solution
        array backup_targets
        string backup_frequency
        boolean has_proxmox_pbs
        boolean has_dr_plan
        string monitoring_solution
        jsonb zabbix_integration
        boolean open_to_shared_licensing
        boolean open_to_backup_replication
        boolean open_to_teams_federation
        boolean open_to_shared_monitoring
        boolean open_to_collective_purchasing
        boolean open_to_knowledge_sharing
        jsonb form_data
        jsonb recommendations
        text audit_errors
    }

    it_collaboration_matches {
        int id PK
        int company_a_id FK "indexed"
        int company_b_id FK "indexed"
        string match_type "indexed: shared_licensing, etc"
        text match_reason
        int match_score "0-100"
        string status "suggested/contacted/in_progress"
        jsonb shared_attributes
        timestamp created_at
    }

    %% ============================================================
    %% MEMBERSHIP MANAGEMENT DOMAIN
    %% ============================================================

    membership_fees {
        int id PK
        int company_id FK "indexed"
        int fee_year "e.g., 2026"
        int fee_month "1-12"
        numeric amount "PLN"
        numeric amount_paid "PLN"
        string status "pending/paid/partial/overdue"
        date payment_date
        string payment_method "transfer/cash/card"
        string payment_reference
        int recorded_by FK
        timestamp recorded_at
        text notes
    }

    membership_fee_config {
        int id PK
        string scope "global/category/company"
        int category_id FK "nullable"
        int company_id FK "nullable"
        numeric monthly_amount "PLN"
        date valid_from
        date valid_until "NULL = active"
        int created_by FK
        text notes
        timestamp created_at
    }

    %% ============================================================
    %% NOTIFICATIONS DOMAIN
    %% ============================================================

    user_notifications {
        int id PK
        int user_id FK "indexed"
        string title
        text message
        string notification_type "news/system/message/event"
        string related_type "company_news/event/message"
        int related_id
        boolean is_read "indexed"
        timestamp read_at
        string action_url
        timestamp created_at "indexed"
    }

    %% ============================================================
    %% RELATIONSHIPS - Core Domain
    %% ============================================================

    users ||--o{ companies : "manages (company_id)"
    companies }o--|| categories : "belongs_to (category_id)"

    companies ||--o{ company_services : "has"
    services ||--o{ company_services : "offered_by"

    companies ||--o{ company_competencies : "has"
    competencies ||--o{ company_competencies : "possessed_by"

    companies ||--o{ certifications : "holds"
    companies ||--o{ awards : "received"
    companies ||--o{ company_events : "has_events"

    %% ============================================================
    %% RELATIONSHIPS - Digital Maturity Domain
    %% ============================================================

    companies ||--o| company_digital_maturity : "has_maturity (1:1)"
    companies ||--o{ company_website_analysis : "analyzed"
    companies ||--o{ maturity_assessments : "assessed"
    companies ||--o| company_quality_tracking : "tracked (1:1)"
    companies ||--o{ company_website_content : "scraped"
    companies ||--o| company_ai_insights : "analyzed_by_ai (1:1)"

    maturity_assessments }o--|| users : "assessed_by"
    company_ai_insights }o--o| company_website_content : "based_on"

    %% ============================================================
    %% RELATIONSHIPS - AI Chat Domain
    %% ============================================================

    users ||--o{ ai_chat_conversations : "owns"
    ai_chat_conversations ||--o{ ai_chat_messages : "contains"
    ai_chat_messages ||--o| ai_chat_feedback : "has_feedback (1:1)"

    ai_chat_feedback }o--|| users : "submitted_by"
    ai_api_costs }o--o| users : "attributed_to"

    %% ============================================================
    %% RELATIONSHIPS - Community Features Domain
    %% ============================================================

    users ||--o{ forum_topics : "created (author_id)"
    users ||--o{ forum_replies : "created (author_id)"
    forum_topics ||--o{ forum_replies : "has_replies"

    users ||--o{ norda_events : "created (created_by)"
    companies ||--o{ norda_events : "speaker_company"
    norda_events ||--o{ event_attendees : "has_attendees"
    users ||--o{ event_attendees : "registered (user_id)"

    users ||--o{ private_messages : "sent (sender_id)"
    users ||--o{ private_messages : "received (recipient_id)"
    private_messages ||--o{ private_messages : "thread (parent_id)"

    users ||--o{ classifieds : "posted (author_id)"
    companies ||--o{ classifieds : "related_to"

    %% ============================================================
    %% RELATIONSHIPS - Social & Contact Domain
    %% ============================================================

    companies ||--o{ company_contacts : "has_contacts"
    companies ||--o{ company_social_media : "has_profiles"
    companies ||--o{ company_recommendations : "recommended"
    users ||--o{ company_recommendations : "recommender"
    users ||--o{ company_recommendations : "moderator"

    %% ============================================================
    %% RELATIONSHIPS - Auditing Systems Domain
    %% ============================================================

    companies ||--o{ gbp_audits : "audited_gbp"
    companies ||--o{ it_audits : "audited_it"
    users ||--o{ it_audits : "auditor"

    companies ||--o{ it_collaboration_matches : "match_company_a"
    companies ||--o{ it_collaboration_matches : "match_company_b"

    %% ============================================================
    %% RELATIONSHIPS - Membership Management Domain
    %% ============================================================

    companies ||--o{ membership_fees : "pays_fees"
    users ||--o{ membership_fees : "recorded_by"

    categories ||--o{ membership_fee_config : "category_fees"
    companies ||--o{ membership_fee_config : "company_fees"
    users ||--o{ membership_fee_config : "configured_by"

    %% ============================================================
    %% RELATIONSHIPS - Notifications Domain
    %% ============================================================

    users ||--o{ user_notifications : "receives"

Key Relationships Explained

One-to-Many Relationships (45+ total)

Parent Child Cardinality Cascade Description
User → Company 1:many No cascade Users can manage multiple companies
Company → Certifications 1:many CASCADE Company certifications auto-delete with company
Company → Awards 1:many CASCADE Company awards auto-delete with company
Company → CompanyEvents 1:many CASCADE Company news/events auto-delete
User → AIChatConversation 1:many CASCADE User's chat history deleted with user
AIChatConversation → AIChatMessage 1:many CASCADE Messages deleted when conversation deleted
User → ForumTopic 1:many CASCADE User's forum topics deleted with user
ForumTopic → ForumReply 1:many CASCADE Replies deleted when topic deleted
NordaEvent → EventAttendee 1:many CASCADE RSVPs deleted when event deleted
Company → MembershipFee 1:many CASCADE Fee records deleted with company
Company → GBPAudit 1:many CASCADE Audit history deleted with company
Company → ITAudit 1:many CASCADE Audit history deleted with company

Many-to-Many Relationships (2 total)

Entity A Junction Table Entity B Description
Company company_services Service Companies can offer multiple services
Company company_competencies Competency Companies can have multiple competencies

Junction Table Pattern:

-- company_services (composite primary key)
PRIMARY KEY (company_id, service_id)
FOREIGN KEY company_id  companies.id
FOREIGN KEY service_id  services.id
+ is_primary (boolean) - flag primary service
+ added_at (timestamp) - when added

One-to-One Relationships (4 total)

Parent Child Constraint Description
Company → CompanyDigitalMaturity 1:1 UNIQUE(company_id) One maturity record per company
Company → CompanyQualityTracking 1:1 UNIQUE(company_id) One quality tracking record
Company → CompanyAIInsights 1:1 UNIQUE(company_id) One AI insights record
AIChatMessage → AIChatFeedback 1:1 UNIQUE(message_id) One feedback per message

Self-Referential Relationships (1 total)

Table Relationship Description
PrivateMessage → PrivateMessage parent_id → id Message threading (conversations)

Many-to-Many (Self-Join) Relationships (1 total)

Table Relationship Description
CompanyCompany via ITCollaborationMatch IT collaboration opportunities between two companies
-- it_collaboration_matches
company_a_id  companies.id
company_b_id  companies.id
UNIQUE(company_a_id, company_b_id, match_type)

Unique Constraints & Indexes

Unique Constraints (20+ total)

Table Column(s) Purpose
users email One account per email
companies slug Unique URL identifier
companies nip One company per NIP (nullable)
categories name, slug Unique category identifiers
services name, slug Unique service identifiers
competencies name, slug Unique competency identifiers
company_digital_maturity company_id One maturity record per company
company_quality_tracking company_id One tracking record per company
company_ai_insights company_id One AI insights per company
ai_chat_feedback message_id One feedback per message
company_contacts (company_id, contact_type, value) Prevent duplicate contacts
company_social_media (company_id, platform, url) Prevent duplicate social links
company_recommendations (user_id, company_id) One recommendation per user-company pair
it_collaboration_matches (company_a_id, company_b_id, match_type) Unique collaboration matches
membership_fees (company_id, fee_year, fee_month) One fee record per company per month

Performance Indexes (60+ total)

Primary Key Indexes (36): All tables have auto-indexed primary key id

Foreign Key Indexes (40+):

  • All company_id columns (indexed for JOIN performance)
  • All user_id columns (indexed for user-related queries)
  • All category_id, service_id, competency_id columns
  • All conversation/topic/event relationship keys

Composite Indexes (5+):

Table Columns Purpose
company_website_analysis (company_id, analyzed_at) Latest analysis queries
gbp_audits (company_id, audit_date) Latest audit queries
it_audits (company_id, audit_date) Latest audit queries
user_notifications (user_id, is_read, created_at) Unread notifications queries
ai_api_costs (timestamp, user_id) Cost tracking queries

Special Indexes:

Table Type Purpose
companies Full-Text Search (tsvector) Fast company search (PostgreSQL FTS)
companies pg_trgm (trigram) Fuzzy matching for typos (when available)

PostgreSQL-Specific Features

Native Data Types

ARRAY Types:

  • Used for: ai_tools_used, m365_plans, server_types, critical_gaps, areas_improved, etc.
  • Storage: PostgreSQL native ARRAY(String)
  • Fallback: JSON string in SQLite
-- Example
ai_tools_used ARRAY(String) -- ['ChatGPT', 'Copilot', 'Gemini']

JSONB Types:

  • Used for: pagespeed_audits, fields_status, recommendations, form_data, zabbix_integration
  • Storage: Binary JSON with indexing support
  • Fallback: JSON string in SQLite
-- Example
pagespeed_audits JSONB -- {'performance': 85, 'seo': 92, ...}

Numeric Types:

  • Numeric(10,2) - Currency (PLN) - e.g., 150.00
  • Numeric(2,1) - Ratings (0.0-5.0) - e.g., 4.5
  • Numeric(3,2) - Confidence scores (0.00-1.00) - e.g., 0.87

Full-Text Search (FTS)

Implementation:

-- companies table has tsvector column for search
search_vector tsvector

-- Trigger updates search_vector on INSERT/UPDATE
CREATE TRIGGER companies_search_trigger
BEFORE INSERT OR UPDATE ON companies
FOR EACH ROW EXECUTE FUNCTION companies_search_trigger();

-- Search query example
SELECT * FROM companies
WHERE search_vector @@ to_tsquery('polish', 'strony & www');

Indexed Columns: name, description_short, description_full, services, competencies

Fuzzy Matching (pg_trgm)

Extension: pg_trgm (Trigram matching)

-- Find companies with similar names (typos)
SELECT * FROM companies
WHERE similarity(name, 'PIXLB') > 0.3  -- matches 'PIXLAB'
ORDER BY similarity(name, 'PIXLB') DESC;

Data Validation & Constraints

Check Constraints

NIP Validation (PostgreSQL):

CONSTRAINT valid_nip CHECK (
  nip ~ '^\d{10}$' OR nip IS NULL
)
-- Ensures NIP is exactly 10 digits or NULL

Email Validation (PostgreSQL):

CONSTRAINT valid_email CHECK (
  email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'
  OR email IS NULL
)
-- Regex validation for email format

Enums (PostgreSQL)

Data Quality Levels:

CREATE TYPE data_quality_level AS ENUM (
  'basic',      -- Minimal data (name, NIP, contact)
  'partial',    -- Some enriched data
  'complete',   -- Full data with verifications
  'verified'    -- Manually verified
);

Company Status:

CREATE TYPE company_status AS ENUM (
  'active',     -- Active company
  'inactive',   -- Inactive company
  'pending',    -- Pending verification
  'archived'    -- Archived
);

Cascade Behaviors

ON DELETE CASCADE (Database Level)

Applied to prevent orphaned records:

Parent Table Child Table Cascade On
companies company_contacts DELETE
companies company_social_media DELETE
companies company_recommendations DELETE
companies gbp_audits DELETE
companies it_audits DELETE
companies it_collaboration_matches DELETE (both company_a and company_b)
companies membership_fees DELETE
users user_notifications DELETE

Effect: When a company or user is deleted, all related audit records, contacts, and notifications are automatically removed.

SQLAlchemy cascade='all, delete-orphan' (ORM Level)

Applied to parent-child relationships where children cannot exist without parent:

# Example from User model
conversations = relationship(
    'AIChatConversation',
    back_populates='user',
    cascade='all, delete-orphan'
)

Applied To:

  • User → AIChatConversation
  • User → ForumTopic
  • User → ForumReply
  • Company → CompanyService (M2M junction)
  • Company → CompanyCompetency (M2M junction)
  • Company → Certification
  • Company → Award
  • Company → CompanyEvent
  • Company → CompanyWebsiteAnalysis
  • Company → MaturityAssessment
  • Company → CompanyWebsiteContent
  • AIChatConversation → AIChatMessage
  • ForumTopic → ForumReply
  • NordaEvent → EventAttendee

Effect: When parent is deleted via SQLAlchemy, all children are deleted. Orphaned children (parent_id becomes NULL) are also deleted.


Database Statistics

Table Statistics

Metric Count
Total Tables 36
Core Domain Tables 10
Digital Maturity Tables 5
AI Chat Tables 4
Community Features 6
Audit Systems 3
Support Tables 8

Relationship Statistics

Relationship Type Count Examples
One-to-Many 45+ Company → Certifications, User → Conversations
Many-to-Many 2 Company ↔ Services, Company ↔ Competencies
One-to-One 4 Company → CompanyDigitalMaturity
Self-Referential 2 PrivateMessage → PrivateMessage, Company ↔ Company (IT matches)

Index Statistics

Index Type Count Purpose
Primary Keys 36 Unique row identifiers
Foreign Key Indexes 60+ JOIN performance
Unique Constraints 20+ Data integrity
Composite Indexes 5+ Multi-column queries
Full-Text Search 1 Company search
Trigram (Fuzzy) 1 Typo tolerance

Schema Evolution & Versioning

Version History

Version Date Changes
1.0 2025-11-23 Initial schema (basic company directory, users, auth)
1.1 2025-11-26 Digital maturity system (CompanyDigitalMaturity, MaturityAssessment)
1.2 2025-12-29 Social media & news monitoring (CompanySocialMedia, CompanyEvents)
1.3 2026-01-09 IT audit & collaboration (ITAudit, ITCollaborationMatch)
1.4 2026-01-10 Current production schema (36 tables)

Migration Strategy

Development:

# Create migration
alembic revision --autogenerate -m "Add IT audit tables"

# Apply migration
alembic upgrade head

Production:

# SSH to NORDABIZ-01
ssh maciejpi@10.22.68.249

# Backup database
pg_dump nordabiz > backup_$(date +%Y%m%d).sql

# Apply migration
cd /var/www/nordabiznes
sudo -u www-data alembic upgrade head

# Verify
psql -U nordabiz_app -d nordabiz -c "\dt"

Best Practices

Query Optimization

DO:

# Use indexed columns in WHERE clauses
companies = db.query(Company).filter(Company.slug == 'pixlab-sp-z-o-o').first()

# Use eager loading for frequently joined relations
companies = db.query(Company).options(joinedload(Company.category)).all()

# Limit result sets
companies = db.query(Company).limit(10).all()

DON'T:

# Avoid SELECT * without WHERE
all_companies = db.query(Company).all()  # loads entire table

# Avoid N+1 queries
for company in companies:
    print(company.category.name)  # separate query for each company

# Avoid unindexed WHERE clauses
companies = db.query(Company).filter(Company.description_full.like('%keyword%')).all()

Data Integrity

Transaction Management:

from database import SessionLocal

db = SessionLocal()
try:
    # Multi-table operation
    company = Company(name="New Company", slug="new-company")
    db.add(company)

    maturity = CompanyDigitalMaturity(company_id=company.id, overall_score=0)
    db.add(maturity)

    db.commit()  # Atomic commit
except Exception as e:
    db.rollback()  # Rollback on error
    raise
finally:
    db.close()

Connection Management

Production Settings:

# Connection pooling (SQLAlchemy default)
engine = create_engine(
    DATABASE_URL,
    pool_size=20,           # Max active connections
    max_overflow=10,        # Max overflow connections
    pool_timeout=30,        # Connection timeout (seconds)
    pool_recycle=3600       # Recycle connections after 1 hour
)

Security

User Permissions:

-- Application user (nordabiz_app) has limited permissions
GRANT CONNECT ON DATABASE nordabiz TO nordabiz_app;
GRANT USAGE ON SCHEMA public TO nordabiz_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO nordabiz_app;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO nordabiz_app;

-- PostgreSQL listens only on localhost (security)
listen_addresses = 'localhost'

Never store sensitive data:

  • Passwords must be hashed (bcrypt)
  • API keys only in .env files (never in database)
  • Credit card data should never be stored

Glossary

Term Definition
PK Primary Key - unique identifier for table row
FK Foreign Key - reference to another table's primary key
UK Unique Key - column(s) that must have unique values
CASCADE Auto-delete related records when parent is deleted
JSONB PostgreSQL binary JSON format (indexed, searchable)
ARRAY PostgreSQL native array type for lists
tsvector PostgreSQL full-text search vector
pg_trgm PostgreSQL trigram extension for fuzzy matching
ORM Object-Relational Mapping (SQLAlchemy)
ERD Entity Relationship Diagram

Maintenance

Regular Tasks

Daily:

  • Monitor database size: SELECT pg_size_pretty(pg_database_size('nordabiz'));
  • Check slow queries: SELECT * FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10;

Weekly:

  • Vacuum analyze: VACUUM ANALYZE;
  • Check index usage: SELECT * FROM pg_stat_user_indexes WHERE idx_scan = 0;

Monthly:

  • Review data quality scores
  • Archive old audit records (>6 months)
  • Optimize indexes if needed

Monitoring Queries

Find missing indexes:

SELECT schemaname, tablename, attname, n_distinct, correlation
FROM pg_stats
WHERE schemaname = 'public'
  AND n_distinct > 100
  AND correlation < 0.1;

Check table sizes:

SELECT
    tablename,
    pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;

  • Flask Components Diagram: 04-flask-components.md - How Flask app uses these models
  • Container Diagram: 02-container-diagram.md - PostgreSQL container details
  • Deployment Architecture: 03-deployment-architecture.md - Database server configuration
  • Database Schema Analysis: ../.auto-claude/specs/003-.../analysis/database-schema.md - Detailed model documentation

Document Version: 1.0 Last Updated: 2026-01-10 Maintained By: Norda Biznes Development Team Next Review: When schema changes are deployed