Infrastructure

How each system is built — services, data flow, and the AWS components behind them.

01
CMS Builder

Django orchestrates the full lifecycle of a Laravel CMS site. Provisioning runs Terraform via subprocess to build the AWS foundation, then boto3 deploys an ECS Fargate service behind an ALB rule. Before launch, Bedrock generates AI copy and images in parallel and bakes them into the container's environment.

Open CMS Manager →
flowchart TD
    Admin([Admin]) -->|deploy| Django[Django\nCMS Manager]

    subgraph Bedrock[AWS Bedrock]
        NL[Nova Lite\ntext generation]
        SIC[Stable Image Core\nimage generation]
    end

    Django -->|generate content| NL
    Django -->|generate images x3| SIC
    NL & SIC -->|AI content + S3 URLs| Django

    Django -->|terraform apply| TF[Terraform\ninfra/]

    subgraph AWS[AWS eu-west-2]
        ALB[Application Load Balancer]
        ECS[ECS Fargate\nLaravel container]
        RDS[(RDS MySQL)]
        S3[(S3 Bucket)]
    end

    TF -->|provisions| AWS
    Django -->|creates service + ALB rule\nwith AI content baked in| ECS
    ECS --> RDS
    ECS --> S3

    Visitor([Visitor]) -->|slug.matt-9.com| ALB --> ECS
                    
Content seeding

The site description drives the entire content pipeline. Django passes it as a prompt to Bedrock, encodes the JSON response as base64, and bakes it directly into the ECS task definition as an environment variable. When the container starts, Laravel's DatabaseSeeder decodes it and seeds the database — pages, posts, and media records — before the first request arrives. No API calls at runtime.

flowchart TD
    Desc(["Site description\neg. a blog about cats"]) -->|prompt| NL["Nova Lite\nhero, about, 3 posts"]
    Desc -->|prompt| SIC["Stable Image Core\nhero image, post covers"]

    NL & SIC -->|JSON + S3 URLs| Encode["base64 encode\nGENERATED_CONTENT_B64"]
    Encode -->|baked into ECS task\nenv vars| ECS["ECS Fargate\nLaravel container"]

    ECS -->|on startup| Seed["DatabaseSeeder.php\ndecode + json_decode"]

    Seed -->|hero title, subtitle, about| Pages[("pages table\nblock JSON")]
    Seed -->|title, excerpt, content| Posts[("posts table")]
    Seed -->|hero image + post cover URLs| Media[("media table\nS3 references")]
                    
Multi-site routing

Each site is created with a name, slug, and description. The description is passed to Bedrock before the container launches — Nova Lite writes the hero copy, about section, and blog posts around it, while Stable Image Core generates matching images. A site described as "a blog about cats" gets entirely different content to one described as "a travel journal about space exploration" — same infrastructure, different world.

flowchart LR
    ALB[Application Load Balancer\nmatt9-alb]

    ALB -->|host: cats.matt-9.com| S1[ECS Service\nmatt9-cats\nLaravel container]
    ALB -->|host: space.matt-9.com| S2[ECS Service\nmatt9-space\nLaravel container]
    ALB -->|host: example.matt-9.com| S3[ECS Service\nmatt9-example\nLaravel container]

    S1 & S2 & S3 -->|each gets its own\ndatabase + S3 prefix| RDS[(RDS MySQL\nlaravel_cats\nlaravel_space\nlaravel_example)]

    Django[Django\nCMS Manager] -->|one ALB listener rule\nper site slug| ALB
    Django -->|one ECS service\nper site slug| S1
    Django -->|one ECS service\nper site slug| S2
    Django -->|one ECS service\nper site slug| S3
                    
02
Media Library

A file manager with a 5 GB per-user quota. Uploads are auto-sorted by type and stored on disk. Admins can also generate images via AWS Bedrock Stable Image Core — the result is saved directly into the library. An approval workflow gates what becomes publicly visible.

Open Media Library →
flowchart TD
    User([User]) -->|upload file| Django[Django\nmedia_system]
    Admin([Admin]) -->|generate prompt| Django

    Django -->|check quota\n5 GB limit| DB[(PostgreSQL\nMedia records)]
    Django -->|sort by type\nimages / videos / docs| FS[Local Storage\n/var/www/django_media]

    Django -->|boto3 invoke| Bedrock[AWS Bedrock\nStable Image Core]
    Bedrock -->|PNG bytes| Django
    Django -->|save generated image| FS

    Django -->|approval status| DB
    DB -->|approved files| Gallery[Gallery view]
    FS -->|serve files| Gallery
    Gallery --> User
                    
03
Handwriting Classifier

A canvas drawing tool with two classification paths. The local path uses a pre-trained EMNIST MLP model loaded once at startup — the image is rotated, cropped, and resized to 28×28 before inference. The AWS path sends the drawing to Bedrock Nova Lite as a vision request and is rate-limited per user per day.

Open Classifier →
flowchart TD
    User([User]) -->|draws character| Canvas[Canvas API\nbase64 PNG]

    Canvas --> Django[Django\nwaes_chat_e]

    Django -->|local model path| Pre[Preprocess\nrotate -90° · crop · 28×28 · normalise]
    Pre --> EMNIST[EMNIST MLP Model\nscikit-learn / joblib\nA–Z · 0–9]
    EMNIST --> Result([Prediction])

    Django -->|AWS path\nrate-limited| Bedrock[AWS Bedrock\nNova Lite vision]
    Bedrock -->|single character| Result
                    
04
Weather Dashboard

A stateless view — no database, no login. On each request Django calls the OpenWeatherMap API with a city name, converts temperatures from Kelvin, and renders current conditions plus a 5-day forecast. Nothing is cached or stored.

Open Weather Dashboard →
flowchart LR
    User([User]) -->|city name| Django[Django\nwaes_weather]
    Django -->|GET current + forecast| OWM[OpenWeatherMap API]
    OWM -->|JSON· Kelvin temps| Django
    Django -->|convert K → °C\nformat forecast| Template[Template]
    Template -->|rendered page| User