Skip to content

Instantly share code, notes, and snippets.

@jghankins
Last active April 15, 2026 00:00
Show Gist options
  • Select an option

  • Save jghankins/81de9e09e35a8384f92b335093bf27ce to your computer and use it in GitHub Desktop.

Select an option

Save jghankins/81de9e09e35a8384f92b335093bf27ce to your computer and use it in GitHub Desktop.
Discovery Directory Architecture Diagrams (Issue #292)

Discovery Directory -- Architecture Diagrams

System Overview

graph TB
    subgraph "Tenant A - ECS Task"
        A_APP[Phoenix App] --> A_DB[(PostgreSQL)]
        A_OBAN["DiscoverySyncWorker<br/>every 15 min"] --> A_APP
        A_OBAN -->|"PutItem"| DYNAMO
    end

    subgraph "Tenant B - ECS Task"
        B_APP[Phoenix App] --> B_DB[(PostgreSQL)]
        B_OBAN["DiscoverySyncWorker<br/>every 15 min"] --> B_APP
        B_OBAN -->|"PutItem"| DYNAMO
    end

    subgraph "Tenant C - ECS Task"
        C_APP[Phoenix App] --> C_DB[(PostgreSQL)]
        C_OBAN["DiscoverySyncWorker<br/>every 15 min"] --> C_APP
        C_OBAN -->|"PutItem"| DYNAMO
    end

    DYNAMO[("DynamoDB<br/>pewpros-discovery<br/>(sync bus)")]

    subgraph "Marketing Site - pewpros.com"
        INGEST["DiscoveryIngestWorker<br/>every 5 min"] -->|"Scan all listings"| DYNAMO
        INGEST -->|"Upsert rows"| PG[(PostgreSQL<br/>discovery_listings<br/>tsvector + GIN indexes)]
        DISCOVER["DiscoverLive"] -->|"Full-text search<br/>+ keyword filter<br/>+ category filter"| PG
        DISCOVERAPI["API: /api/v1/discover"] -->|"Ecto queries"| PG
    end

    BROWSER[Browser] --> DISCOVER
    MOBILE[Mobile App] --> DISCOVERAPI
Loading

Data Flow

flowchart LR
    subgraph "Each Tenant"
        LOCAL_DB[(Tenant DB)] --> SYNC["DiscoverySyncWorker"]
    end

    SYNC -->|"ExAws PutItem<br/>every 15 min"| DYNAMO[("DynamoDB<br/>sync bus")]

    subgraph "Marketing Site"
        DYNAMO -->|"Scan every 5 min"| INGEST["DiscoveryIngestWorker"]
        INGEST -->|"Ecto upsert"| PG[("Postgres<br/>discovery_listings")]
        PG -->|"tsvector search"| LIVE["DiscoverLive"]
        PG -->|"Ecto queries"| API["JSON API"]
    end
Loading

Sync Worker Flow

flowchart TD
    START(["DiscoverySyncWorker runs"]) --> CHECK_OPT{"discovery_enabled?"}
    CHECK_OPT -->|No| DELETE["Delete listing from DynamoDB"]
    CHECK_OPT -->|Yes| CHECK_ELIG{"Meets eligibility?"}

    CHECK_ELIG -->|No| DELETE
    DELETE --> DONE(["Return ok"])

    CHECK_ELIG -->|Yes| COLLECT["Collect local data"]

    COLLECT --> TENANT["Tenant profile<br/>name, domain, logo, bio"]
    COLLECT --> STATS["Community stats<br/>members, active, posts"]
    COLLECT --> EVENTS["Upcoming events<br/>top 3 by date"]
    COLLECT --> COURSES["Published courses<br/>top 3 by enrollments"]
    COLLECT --> KEYWORDS["Keywords + categories<br/>from admin settings"]

    TENANT --> SCORE["Compute ranking score"]
    STATS --> SCORE
    EVENTS --> SCORE
    COURSES --> SCORE
    KEYWORDS --> SCORE

    SCORE --> WRITE["Write to DynamoDB"]
    WRITE --> META["PutItem sk=METADATA<br/>full profile + keywords"]
    WRITE --> CATS["BatchWriteItem sk=CAT items<br/>one per category"]
    WRITE --> CLEANUP["Delete stale CAT items"]

    META --> DONE
    CATS --> DONE
    CLEANUP --> DONE
Loading

Postgres Search Architecture

graph TB
    subgraph "discovery_listings table"
        COLS["name | tagline | bio | keywords | categories<br/>ranking_score | stats | featured_events | ..."]
        TSVEC["search_vector TSVECTOR GENERATED ALWAYS AS<br/>weight A: name + keywords<br/>weight B: tagline + categories<br/>weight C: bio"]
    end

    subgraph "Indexes"
        GIN_FTS["GIN index on search_vector<br/>(full-text search)"]
        GIN_TRGM["GIN index on name with pg_trgm<br/>(fuzzy / typo tolerance)"]
        GIN_KW["GIN index on keywords array<br/>(keyword filtering)"]
        GIN_CAT["GIN index on categories array<br/>(category filtering)"]
        BTREE["B-tree on site_type + ranking_score<br/>(browse + sort)"]
    end

    COLS --> TSVEC
    TSVEC --> GIN_FTS
    COLS --> GIN_TRGM
    COLS --> GIN_KW
    COLS --> GIN_CAT
    COLS --> BTREE
Loading

Search Query Examples

flowchart LR
    USER["User types:<br/>concealed carry"] --> FTS{"Full-text search<br/>plainto_tsquery"}
    FTS --> RANK["Results ranked by<br/>ts_rank with weights<br/>A: name + keywords<br/>B: tagline<br/>C: bio"]

    USER2["User types:<br/>detriot"] --> TRGM{"Trigram similarity<br/>pg_trgm"}
    TRGM --> FUZZY["Matches Detroit<br/>despite typo"]

    USER3["User clicks:<br/>beginner-friendly pill"] --> ARRAY{"Array overlap<br/>keywords && ARRAY"}
    ARRAY --> FILTERED["All communities<br/>tagged beginner-friendly"]
Loading

DynamoDB Data Model

classDiagram
    class DynamoDB_SyncBus {
        +String pk : LISTING_tenant_slug
        +String sk : METADATA or CAT_category
        +String site_type : pewpros or coachpros
        +String tenant_slug
        +String name
        +String domain
        +String tagline
        +String bio
        +List keywords : admin configured tags
        +List categories
        +Object stats
        +List featured_events
        +List featured_courses
        +Number ranking_score : 0 to 100
        +Number ttl : 7 day auto expire
    }

    class Postgres_SearchTable {
        +BigInt id : auto increment
        +String tenant_slug : unique
        +String site_type
        +String name
        +Text tagline
        +Text bio
        +Array keywords : text array
        +Array categories : text array
        +JSONB stats
        +JSONB featured_events
        +JSONB featured_courses
        +Float ranking_score
        +TSVECTOR search_vector : generated
        +Timestamp synced_at
    }

    DynamoDB_SyncBus --> Postgres_SearchTable : "DiscoveryIngestWorker<br/>scans and upserts<br/>every 5 min"
Loading

Ranking Score Composition

pie title Ranking Score Weights - 100 points total
    "Member Count - 25" : 25
    "Engagement - 25" : 25
    "Content Richness - 20" : 20
    "Event Activity - 15" : 15
    "Profile Completeness - 10" : 10
    "Freshness - 5" : 5
Loading

Access Patterns

graph LR
    subgraph "Write Path - per tenant via ExAws"
        SYNC["Oban Sync Worker"] -->|"PutItem<br/>full listing + keywords"| DYNAMO[("DynamoDB")]
    end

    subgraph "Ingestion - marketing site"
        DYNAMO -->|"Scan every 5 min"| INGEST["Ingest Worker"]
        INGEST -->|"Ecto upsert"| PG[("Postgres")]
    end

    subgraph "Read Paths - Ecto queries"
        BROWSE["Browse All"] -->|"WHERE site_type<br/>ORDER BY ranking_score"| PG
        FILTER["Filter Category"] -->|"WHERE categories @> ARRAY"| PG
        KEYWORD["Filter Keyword"] -->|"WHERE keywords && ARRAY"| PG
        SEARCH["Full-text Search"] -->|"WHERE search_vector @@ tsquery<br/>ORDER BY ts_rank"| PG
        FUZZY["Fuzzy Search"] -->|"WHERE similarity > 0.3<br/>pg_trgm"| PG
        DETAIL["View Detail"] -->|"WHERE tenant_slug = ?"| PG
    end
Loading

IAM Security Model

graph TB
    subgraph "Tenant ECS Task - detroitarms"
        TASK_ROLE["IAM Task Role"]
    end

    subgraph "DynamoDB pewpros-discovery"
        OWN["pk=LISTING_detroitarms"]
        OTHER["pk=LISTING_other-tenant"]
    end

    TASK_ROLE -->|"PutItem, DeleteItem<br/>Condition: LeadingKeys = LISTING_detroitarms"| OWN
    TASK_ROLE -.->|"DENIED by IAM condition"| OTHER

    subgraph "Marketing Site - pewpros.com"
        MKT_ROLE["IAM Task Role"]
    end

    MKT_ROLE -->|"Scan - read all items<br/>for ingestion into Postgres"| OWN
    MKT_ROLE -->|"Scan - read all items<br/>for ingestion into Postgres"| OTHER
Loading

Phased Rollout

gantt
    title Discovery Directory Rollout
    dateFormat YYYY-MM-DD

    section Phase 1 - Infra and Sync
        DynamoDB table and IAM policies     :p1a, 2026-04-21, 3d
        ex_aws_dynamo and Discovery module  :p1b, after p1a, 3d
        Postgres table and tsvector indexes :p1f, after p1a, 2d
        DiscoverySyncWorker                 :p1c, after p1b, 2d
        DiscoveryIngestWorker               :p1g, after p1f, 2d
        Admin settings with keywords        :p1d, after p1b, 2d
        Testing and verification            :p1e, after p1g, 2d

    section Phase 2 - UI and API
        DiscoverLive Index with search      :p2a, after p1e, 3d
        DiscoverLive Show                   :p2b, after p2a, 2d
        API controller and routes           :p2c, after p1e, 2d
        Design system and polish            :p2d, after p2b, 3d

    section Phase 3 - MCP and Mobile
        MCP tools                           :p3a, after p2d, 2d
        Admin moderation                    :p3b, after p2d, 2d
        Mobile app integration              :p3c, after p3a, 3d

    section Phase 4 - Enhanced Search
        Geo search with PostGIS             :p4a, after p3c, 3d
        Faceted filtering                   :p4b, after p4a, 3d
        Search analytics                    :p4c, after p4b, 2d
Loading

Discovery UI Wireframe - Conceptual

graph TB
    subgraph "Directory Page"
        HEADER["Search Bar: full-text + fuzzy"]
        KEYWORDS["Keyword Pills: beginner-friendly / ccw / youth / competition"]
        CATS["Category Filter: Firearms Training / Range / Tactical"]
        SORT["Sort: Best / Newest / Members / Relevance"]

        subgraph "Community Cards Grid"
            CARD1["Logo + Name<br/>Tagline<br/>Keywords: ccw, beginner-friendly<br/>342 members / 15 events<br/>Detroit, MI"]
            CARD2["Logo + Name<br/>Tagline<br/>Keywords: tactical, competition<br/>128 members / 8 events<br/>Austin, TX"]
            CARD3["Logo + Name<br/>Tagline<br/>Keywords: youth, outdoor<br/>89 members / 3 events<br/>Nashville, TN"]
        end

        PAGINATION["Load More"]
    end

    HEADER --> KEYWORDS --> CATS --> SORT --> CARD1
    CARD1 --- CARD2 --- CARD3 --> PAGINATION

    CARD1 -->|Click| DETAIL_PAGE

    subgraph "Detail Page"
        DETAIL_PAGE["Cover Image + Logo<br/>Community Name<br/>Keyword Tags<br/>Bio and Description<br/>---<br/>Stats: 342 members / 15 events / 8 courses<br/>---<br/>Featured Events<br/>Featured Courses<br/>---<br/>Visit Community Button"]
    end
Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment