- 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>
1234 lines
37 KiB
Markdown
1234 lines
37 KiB
Markdown
# 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
|
|
|
|
```mermaid
|
|
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:**
|
|
```sql
|
|
-- 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 |
|
|
|-------|--------------|-------------|
|
|
| **Company** ↔ **Company** | via ITCollaborationMatch | IT collaboration opportunities between two companies |
|
|
|
|
```sql
|
|
-- 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
|
|
|
|
```sql
|
|
-- 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
|
|
|
|
```sql
|
|
-- 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:**
|
|
```sql
|
|
-- 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)
|
|
|
|
```sql
|
|
-- 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):**
|
|
```sql
|
|
CONSTRAINT valid_nip CHECK (
|
|
nip ~ '^\d{10}$' OR nip IS NULL
|
|
)
|
|
-- Ensures NIP is exactly 10 digits or NULL
|
|
```
|
|
|
|
**Email Validation (PostgreSQL):**
|
|
```sql
|
|
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:**
|
|
```sql
|
|
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:**
|
|
```sql
|
|
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:
|
|
|
|
```python
|
|
# 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:**
|
|
```bash
|
|
# Create migration
|
|
alembic revision --autogenerate -m "Add IT audit tables"
|
|
|
|
# Apply migration
|
|
alembic upgrade head
|
|
```
|
|
|
|
**Production:**
|
|
```bash
|
|
# 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:**
|
|
```python
|
|
# 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:**
|
|
```python
|
|
# 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:**
|
|
```python
|
|
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:**
|
|
```python
|
|
# 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:**
|
|
```sql
|
|
-- 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:**
|
|
```sql
|
|
SELECT schemaname, tablename, attname, n_distinct, correlation
|
|
FROM pg_stats
|
|
WHERE schemaname = 'public'
|
|
AND n_distinct > 100
|
|
AND correlation < 0.1;
|
|
```
|
|
|
|
**Check table sizes:**
|
|
```sql
|
|
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;
|
|
```
|
|
|
|
---
|
|
|
|
## Related Documentation
|
|
|
|
- **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
|