docs/architecture.md

Reef-A-Matic Architecture

This document maps the current Reef-A-Matic application from the UI to API Gateway, Lambda functions, SQS queues, S3 buckets, EventBridge, DynamoDB tables, Stripe, Cognito, and the GitHub Actions deployment path.

It is based on the current implementation in:

  • apps/ui/src/components/*
  • apps/ui/src/api/*
  • terraform/api-gateway.tf
  • terraform/lambda.tf
  • terraform/sqs.tf
  • terraform/ws-gateway.tf
  • terraform/event-bridge.tf
  • terraform/dyanmodb.tf
  • .github/workflows/deploy.yml
  • stripe/

System Overview

flowchart LR
    subgraph Client["Browser Client"]
        LP["Landing / Login"]
        APP["CustomAppLayout"]
        HOME["Home dashboard"]
        DASHEDIT["Dashboard editor"]
        TEMPLATEEDIT["Template editor"]
        UP["ICPUpload modal"]
        ICP["ICP test detail"]
        TREND["Element trend modal"]
        TEST["Manual test detail"]
        DOSE["Dose history views"]
        PROGRAM["Program history views"]
        PROFILE["User profile / billing modal"]
        TANKSETTINGS["Tank settings"]
        ADMIN["Admin page"]
        NAV["Side navigation + websocket status"]
    end

    subgraph Auth["Auth"]
        COG["Amazon Cognito"]
        SES["Amazon SES: reefamatic.com sender identity"]
        R53MAIL["Route 53: SES verification, DKIM, MAIL FROM"]
        PRESIGNUP["Lambda: user-cognito-pre-register"]
        POSTSIGNUP["Lambda: user-cognito-post-register"]
    end

    subgraph Edge["Frontend Hosting"]
        CF["CloudFront"]
        UIB["UI S3 bucket"]
        DOCSCF["Docs CloudFront"]
        DOCSB["Docs S3 bucket"]
    end

    subgraph Deploy["GitHub Actions / Terraform"]
        DEVBR["dev branch"]
        PRODBR["prod branch"]
        WF["Build and Deploy Platform"]
        TF["Terraform env state"]
        CFG["Resolve env API, WS, Cognito, CloudFront"]
        DOCSGEN["Render architecture docs"]
    end

    subgraph Observability["Operations Observability"]
        CWDASH["CloudWatch dashboard: env-reef-a-matic-operations"]
        GRAFANA["Amazon Managed Grafana: env-reefamatic-ops"]
        OPSALERT["SNS: env-reefamatic-ops-alerts"]
        CWALARMS["CloudWatch alarms"]
        CWLOGS["CloudWatch Logs Insights"]
        CE["AWS Cost Explorer: service + tag costs"]
    end

    subgraph Stripe["Stripe"]
        SCAT["Stripe catalog Terraform"]
        CHECKOUT["Stripe Checkout"]
        PORTAL["Stripe Billing Portal"]
        SWH["Stripe webhook events"]
    end

    subgraph Api["REST API Gateway"]
        U1["POST /user"]
        U2["POST /user/save"]
        U3["POST /user/billing/checkout"]
        U4["POST /user/billing/portal"]
        U5["POST /user/billing/friend-access"]
        U6["POST /user/billing/webhook"]
        U7["POST /user/admin/config"]
        U8["POST /user/admin/config/save"]
        U9["POST /user/admin/errors"]
        U10["POST /user/admin/overview"]
        U11["POST /user/dashboard"]
        U12["POST /user/dashboard/save"]
        U13["POST /user/dashboard/delete"]
        U14["POST /user/template"]
        U15["POST /user/template/save"]
        U16["POST /user/template/delete"]
        U17["POST /user/admin/costs"]
        U18["POST /user/target-ranges"]
        U19["POST /user/reef-reviews"]

        T1["POST /test"]
        T2["POST /test/elements/list"]
        T3["POST /test/history"]
        T4["POST /test/history/icp"]
        T5["POST /test/history/list"]
        T6["POST /test/history/save"]
        T7["POST /test/history/icp/trend"]
        C1["POST /corrections"]

        D1["POST /dose/elements/list"]
        D2["POST /dose/history"]
        D3["POST /dose/history/list"]
        D4["POST /dose/history/save"]
        D5["POST /dose/history/delete"]

        P1["POST /program/history"]
        P2["POST /program/history/list"]
        P3["POST /program/history/save"]
        P4["POST /program/history/delete"]
    end

    subgraph UploadPath["ICP Upload / Processing"]
        PRESIGN["Lambda: upload-presigned"]
        S3U["S3: env-reef-a-matic-upload"]
        CONVERT["Lambda: upload-convert"]
        XVAL["Extraction validation gate"]
        Q2["SQS FIFO: env-upload-results.fifo"]
        EVAL["Lambda: corrections-eval-element-test-result"]
    end

    subgraph AppLambdas["App Lambdas"]
        LUSER1["Lambda: user-get-user-profile"]
        LUSER2["Lambda: user-save-user-profile"]
        LUSER3["Lambda: user-billing-checkout"]
        LUSER4["Lambda: user-billing-portal"]
        LUSER5["Lambda: user-billing-friend-access"]
        LUSER6["Lambda: user-billing-webhook"]
        LUSER7["Lambda: user-admin-config-get"]
        LUSER8["Lambda: user-admin-config-save"]
        LUSER9["Lambda: user-admin-errors-list"]
        LUSER10["Lambda: user-admin-overview-get"]
        LUSER11["Lambda: user-dashboard-list"]
        LUSER12["Lambda: user-dashboard-save"]
        LUSER13["Lambda: user-dashboard-delete"]
        LUSER14["Lambda: user-entry-template-list"]
        LUSER15["Lambda: user-entry-template-save"]
        LUSER16["Lambda: user-entry-template-delete"]
        LUSER17["Lambda: user-admin-costs-get"]
        LUSER18["Lambda: user-target-ranges"]
        LUSER19["Lambda: user-reef-reviews"]

        LTEST1["Lambda: test-list-test-elements"]
        LTEST2["Lambda: test-get-test-history"]
        LTEST3["Lambda: test-get-icp-test-history"]
        LTEST4["Lambda: test-list-test-history"]
        LTEST5["Lambda: test-save-test-history"]
        LTEST6["Lambda: test-get-icp-element-trend"]

        LCORR["Lambda: corrections-apply-correction"]

        LDOSE1["Lambda: dose-list-dose-elements"]
        LDOSE2["Lambda: get-dose-history"]
        LDOSE3["Lambda: dose-list-dose-history"]
        LDOSE4["Lambda: dose-save-dose-history-apigw"]
        LDOSE5["Lambda: dose-delete-dose-history"]
        LDOSE6["Lambda: save-dose-history-sqs"]

        LPROG1["Lambda: dose-get-program-history"]
        LPROG2["Lambda: dose-list-program-history"]
        LPROG3["Lambda: dose-save-program-history"]
        LPROG4["Lambda: dose-delete-program-history"]
        LPROG5["Lambda: generate-past-program-history"]
        LPROG6["Lambda: dose-save-daily-dose"]
        LPROG7["Lambda: dose-apply-daily-dose"]
    end

    subgraph AsyncDose["Dose / Program Async"]
        Q3["SQS FIFO: env-icp-correction.fifo"]
        Q4["SQS: env-dose-save-daily-dose"]
        Q5["SQS: env-dose-generate-past-dose"]
        EB["EventBridge rule: apply-daily-dose (01:00 UTC)"]
    end

    subgraph Realtime["WebSocket API"]
        WS["API Gateway WebSocket"]
        WSC["Lambda: ws-test-history-connect"]
        WSD["Lambda: ws-test-history-disconnect"]
        WST["Lambda: ws-test-history-notify"]
        WSDOSE["Lambda: ws-dose-history-notify"]
        WSPROG["Lambda: ws-program-history-notify"]
        WSTBL["DynamoDB: env-websocket-connections"]
    end

    subgraph Data["DynamoDB"]
        USER["env-user"]
        TANK["env-user-tank"]
        TESTH["env-test-history"]
        TESTE["env-test-element"]
        DOSEH["env-dose-history"]
        DOSEE["env-dose-element"]
        PROGH["env-program-history"]
        DAILY["env-daily-dose"]
        DRAFTFB["env-program-draft-feedback"]
        CORR["env-element-correction"]
        TARGETOVR["env-element-target-override"]
        REEFREV["env-reef-review"]
        BATCH["env-batch-history"]
        TDH["env-tank-dose-history"]
        TDE["env-tank-dose-elements"]
        APPCFG["env-app-config"]
        APPERR["env-app-error"]
        USAGE["env-usage-event"]
        FRIEND["env-friend-access-grant"]
        USERDASH["env-user-dashboard"]
        TEMPLATE["env-user-entry-template"]
    end

    DEVBR --> WF
    PRODBR --> WF
    WF --> CFG
    WF --> TF
    WF --> UIB
    WF --> CF
    WF --> DOCSGEN
    DOCSGEN --> DOCSB
    DOCSB --> DOCSCF
    TF --> CWDASH
    TF --> GRAFANA
    TF --> OPSALERT
    TF --> CWALARMS

    LP --> COG
    COG --> PRESIGNUP
    COG --> POSTSIGNUP
    APP --> CF --> UIB
    APP --> COG
    APP --> U1
    APP --> T5
    APP --> D3
    APP --> P2
    NAV --> WS

    PROFILE --> U1
    PROFILE --> U2
    PROFILE --> U3
    PROFILE --> U4
    PROFILE --> U5
    TANKSETTINGS --> U18
    ICP --> U19
    ADMIN --> U7
    ADMIN --> U8
    ADMIN --> U9
    ADMIN --> U10
    ADMIN --> U17

    UP --> T1
    UP --> S3U
    ICP --> T4
    ICP --> C1
    TREND --> T7
    TREND --> T8
    TEST --> T2
    TEST --> T3
    TEST --> T6
    HOME --> T5
    HOME --> D1
    HOME --> D2
    HOME --> U11
    DASHEDIT --> D1
    DASHEDIT --> U11
    DASHEDIT --> U12
    DASHEDIT --> U13
    TEMPLATEEDIT --> D1
    TEMPLATEEDIT --> T2
    TEMPLATEEDIT --> U14
    TEMPLATEEDIT --> U15
    TEMPLATEEDIT --> U16
    DOSE --> D1
    DOSE --> D2
    DOSE --> D3
    DOSE --> D4
    DOSE --> D5
    DOSE --> U14
    TEST --> U14
    PROGRAM --> P1
    PROGRAM --> P2
    PROGRAM --> P3
    PROGRAM --> P4

    U1 --> LUSER1
    U2 --> LUSER2
    U3 --> LUSER3
    U4 --> LUSER4
    U5 --> LUSER5
    U6 --> LUSER6
    U7 --> LUSER7
    U8 --> LUSER8
    U9 --> LUSER9
    U10 --> LUSER10
    U11 --> LUSER11
    U12 --> LUSER12
    U13 --> LUSER13
    U14 --> LUSER14
    U15 --> LUSER15
    U16 --> LUSER16
    U17 --> LUSER17
    U18 --> LUSER18
    U19 --> LUSER19
    LUSER18 --> TANK
    LUSER18 --> TESTE
    LUSER18 --> CORR
    LUSER18 --> TARGETOVR
    LUSER19 --> REEFREV
    LUSER19 --> TESTH
    LUSER19 --> PROGH
    LUSER17 --> CE
    LUSER17 --> USAGE
    LUSER17 --> TESTH
    LUSER17 --> USER

    T1 --> PRESIGN
    T2 --> LTEST1
    T3 --> LTEST2
    T4 --> LTEST3
    T5 --> LTEST4
    T6 --> LTEST5
    T7 --> LTEST6
    C1 --> LCORR

    D1 --> LDOSE1
    D2 --> LDOSE2
    D3 --> LDOSE3
    D4 --> LDOSE4
    D5 --> LDOSE5

    P1 --> LPROG1
    P2 --> LPROG2
    P3 --> LPROG3
    P4 --> LPROG4

    PRESIGN --> S3U
    S3U --> CONVERT
    CONVERT --> XVAL
    XVAL -->|"load validation_required source=tank elements"| TESTE
    XVAL -->|"valid required panel"| Q2
    XVAL -->|"invalid metadata or duplicates"| TESTH
    Q2 --> EVAL

    LCORR --> Q3
    Q3 --> LDOSE6

    LPROG3 --> Q4
    LPROG3 --> Q5
    Q4 --> LPROG6
    Q5 --> LPROG5
    EB --> LPROG7
    LPROG7 --> Q4

    LUSER1 --> USER
    LUSER1 --> TANK
    LUSER2 --> USER
    LUSER2 --> TANK
    LUSER3 --> USER
    LUSER3 --> CHECKOUT
    LUSER4 --> USER
    LUSER4 --> PORTAL
    LUSER5 --> USER
    LUSER5 --> FRIEND
    LUSER6 --> SWH
    LUSER6 --> USER
    LUSER7 --> APPCFG
    LUSER8 --> APPCFG
    LUSER9 --> APPERR
    LUSER10 --> USER
    LUSER10 --> TANK
    LUSER10 --> TESTH
    LUSER10 --> DOSEH
    LUSER10 --> PROGH
    LUSER10 --> APPERR
    LUSER10 --> USAGE
    LUSER11 --> USERDASH
    LUSER12 --> USER
    LUSER12 --> USERDASH
    LUSER13 --> USERDASH
    LUSER14 --> TEMPLATE
    LUSER15 --> TEMPLATE
    LUSER16 --> TEMPLATE
    POSTSIGNUP --> USER
    POSTSIGNUP --> FRIEND

    LTEST1 --> TESTE
    LTEST2 --> TESTH
    LTEST3 --> TESTH
    LTEST3 --> PROGH
    LTEST3 --> DOSEH
    LTEST6 --> TESTH
    LTEST6 --> DOSEH
    LTEST6 --> CORR
    LTEST6 --> TARGETOVR
    LTEST6 --> TANK
    LTEST4 --> TESTH
    LTEST5 --> TESTH

    CONVERT --> TESTH
    EVAL --> TESTH
    EVAL --> CORR
    EVAL --> TANK
    EVAL --> REEFREV

    LCORR --> TESTH
    LCORR --> PROGH
    LCORR --> DOSEH
    LCORR --> REEFREV

    LDOSE1 --> DOSEE
    LDOSE2 --> DOSEH
    LDOSE3 --> DOSEH
    LDOSE4 --> DOSEH
    LDOSE5 --> DOSEH
    LDOSE6 --> DOSEH
    LDOSE6 --> TDH
    LDOSE6 --> TDE

    LPROG1 --> PROGH
    LPROG2 --> PROGH
    LPROG3 --> PROGH
    LPROG4 --> PROGH
    LPROG5 --> PROGH
    LPROG5 --> DOSEH
    LPROG6 --> DAILY
    LPROG7 --> DAILY
    LPROG7 --> DOSEH
    LPROG3 --> DRAFTFB
    LPROG3 --> REEFREV

    TESTH --> WST
    DOSEH --> WSDOSE
    PROGH --> WSPROG
    LUSER9 --> APPERR
    AppLambdas --> CWLOGS
    CWDASH --> CWLOGS
    CWDASH --> AppLambdas
    CWDASH --> Api
    CWDASH --> AsyncDose
    CWDASH --> Data
    GRAFANA --> CWLOGS
    GRAFANA --> AppLambdas
    CWALARMS --> OPSALERT
    WS --> WSC
    WS --> WSD
    WSC --> WSTBL
    WSD --> WSTBL
    WST --> WSTBL
    WSDOSE --> WSTBL
    WSPROG --> WSTBL

Environment Isolation And Deployment Flow

flowchart TB
    DEV["dev branch"] --> GHA["Build and Deploy Platform workflow"]
    PROD["prod branch"] --> GHA

    GHA --> DETECT["Detect changed components"]
    DETECT --> BUILDUI["Build UI when apps/ui changed"]
    DETECT --> BUILDDOCS["Render architecture docs when docs changed"]
    DETECT --> BUILDBE["Build changed Lambda modules"]
    DETECT --> APPLYTF["Apply Terraform when backend or terraform changed"]

    BUILDUI --> RESOLVE["Resolve config from AWS for target env"]
    RESOLVE --> REST["REST API id: env-rest-api"]
    RESOLVE --> WSAPI["WebSocket API id: env-websocket-api with env stage + $connect"]
    RESOLVE --> COGPOOL["Cognito pool/client: env-user-pool/env-client"]
    RESOLVE --> DOMAIN["App URL: dev.reefamatic.com or www.reefamatic.com"]
    RESOLVE --> GUARD["Prod guard rejects dev URLs, dev Cognito pool, or dev domain"]
    GUARD --> UIS3["Sync UI dist to env-reef-a-matic-ui"]
    UIS3 --> CFDIST["Resolve CloudFront distribution by domain"]
    CFDIST --> INVALIDATE["Invalidate /*"]

    BUILDDOCS --> DOCSHTML["Generate static HTML from markdown + Mermaid"]
    DOCSHTML --> DOCSS3["Sync to env-reef-a-matic-docs"]
    DOCSS3 --> DOCSCF["Invalidate docs CloudFront distribution"]

    BUILDBE --> ARTIFACTS["Upload changed jars to env-reef-a-matic-api"]
    ARTIFACTS --> APPLYTF
    APPLYTF --> TFSTATE["S3 state key: terraform/env/reef-a-matic-api.tfstate"]
    APPLYTF --> AWS["Environment-scoped AWS resources"]
    BUILDBE --> SEED["Refresh reference seed JSON"]
    SEED --> LOADER["Invoke seed loaders"]
    LOADER --> TESTELEMENT["env-test-element reference table"]
    LOADER --> DOSEELEMENT["env-dose-element method catalogs"]
    LOADER --> CORRECTIONREF["env-element-correction method metadata"]
  • Pull requests build and validate but do not deploy.
  • Merging to dev deploys the dev environment.
  • Merging to prod deploys the prod environment.
  • The UI no longer trusts repo-level VITE_* variables for deployed builds. It resolves API, WebSocket, Cognito, and redirect config from AWS for the target environment.
  • WebSocket config resolution ignores empty duplicate API shells by requiring the API to have the target environment stage and a $connect route.
  • When the test module changes, deployment refreshes the Oceamo element seed JSON and invokes the test seed loader so env-test-element has current validation_required flags before new uploads rely on them.
  • When dose or correction modules change, deployment refreshes method-specific dose catalogs and correction metadata. Triton, Fauna Marin, and Custom / No Method are seeded as separate dose brands/correction versions so UI drawers and backend categorization can follow the tank's selected dosing method.
  • Prod UI builds fail if the compiled artifact contains known dev API URLs, the dev WebSocket URL pattern, the dev Cognito user pool id, or dev.reefamatic.com.
  • CloudFront invalidation is resolved by the target domain, avoiding cross-environment distribution variables.

Operations Observability

Terraform provisions an AWS-native operations baseline without Prometheus:

  • Amazon Managed Grafana workspace ${environment}-reefamatic-ops with CloudWatch and X-Ray data sources
  • CloudWatch dashboard ${environment}-reef-a-matic-operations
  • SNS alarm topic ${environment}-reefamatic-ops-alerts
  • CloudWatch alarms for REST API 5XX errors, selected Lambda errors, SQS oldest message age, and DynamoDB throttled requests

The CloudWatch operations dashboard has an Environment filter with dev and prod, so operators can switch between environments instead of reading combined charts. Alarms are deployed per environment to avoid dev and prod Terraform states managing the same alarm names.

The recent Lambda errors widget queries selected Lambda log groups with separate Logs Insights SOURCE clauses and a shared piped query body, preventing CloudWatch from receiving a comma-joined log group name when the dashboard starts a query.

ICP Upload And Evaluation Flow

sequenceDiagram
    participant User
    participant UI as ICPUpload modal
    participant API as REST API Gateway
    participant Pre as upload-presigned
    participant S3 as S3 upload bucket
    participant Convert as upload-convert
    participant Validate as extraction validation gate
    participant TestElement as DynamoDB test-element
    participant QEval as SQS upload-results.fifo
    participant Eval as corrections-eval-element-test-result
    participant DailyDraft as Shared DailyDoseDraftProgramService
    participant ProgramHistory as DynamoDB program-history
    participant ReefReview as DynamoDB reef-review
    participant TestHistory as DynamoDB test-history
    participant WS as WebSocket notifier

    User->>UI: Select 1..n PDFs and click Upload
    UI->>API: POST /test validateOnly with requested file count
        API->>Pre: enforce upload allowance
        Pre-->>UI: allowed or entitlement error
    loop per file
        UI->>API: POST /test
        API->>Pre: invoke
        Pre->>TestHistory: Create upload placeholder / PROCESSING state with selected ICP lab brand
        Pre-->>UI: signed S3 PUT URL + test id
        UI->>UI: Show immediate "uploaded, processing" indicator
        UI->>S3: PUT PDF bytes
        S3->>Convert: S3 notification
        Convert->>TestHistory: Read upload placeholder to resolve ICP lab brand
        Convert->>Convert: Extract with Bedrock using the selected lab panel and override canonical metadata and RO/Osmose rows from PDF text
        Convert->>TestHistory: Save/refresh ICP row as IN_PROGRESS
        Convert->>Validate: Canonicalize and validate extracted metadata + reference tank panel
        Validate->>TestElement: Load validation_required source=tank panel for the ICP brand
        alt extraction is structurally valid
            Validate->>TestHistory: Add MISSING placeholders for absent expected elements in their normal category
            Validate->>QEval: Send one message per non-RO extracted element
            QEval->>Eval: invoke
            Eval->>TestHistory: Enrich categories, messages, targets, corrections, trend signals
            Eval->>DailyDraft: Generate Pro daily-dose draft suggestions from active program + tank history
            DailyDraft->>ProgramHistory: Create/update draft program
            Eval->>ReefReview: Create/update guided Reef Review linked to draft
            Eval->>TestHistory: Mark test DONE only after expected elements finish
        else extraction has missing metadata or duplicate element names
            Validate->>TestHistory: Mark test ERROR and skip corrections/draft generation
        end
        TestHistory->>WS: DynamoDB stream
        WS-->>UI: TEST status update for left nav / detail
    end

ICP Trend And Predictive Detail Flow

sequenceDiagram
    participant User
    participant Card as ICP element card
    participant API as REST API Gateway
    participant Trend as test-get-icp-element-trend
    participant TankSettings as user-target-ranges
    participant TestHistory as DynamoDB test-history
    participant DoseHistory as DynamoDB dose-history
    participant Corrections as DynamoDB element-correction
    participant Overrides as DynamoDB element-target-override

    User->>Card: Click chart icon
    Card->>API: POST /test/history/icp/trend
    API->>Trend: invoke
    Trend->>TestHistory: Load historical measured values for tank/element
    Trend->>DoseHistory: Load related dosing history
    Trend->>Corrections: Load Moonshiners target guidance
    Trend->>Overrides: Load tank-level custom target if present
    alt Pro or friend/pro-equivalent access
        Trend-->>Card: Measured history, dose history, target range, explanation, future projection
    else Basic access
        Trend-->>Card: Measured history and target range, no future projection
    end
    opt User manages tank target ranges
        User->>API: POST /user/target-ranges
        API->>TankSettings: invoke
        TankSettings->>Overrides: Close active range and create new active version, or reset to default
        TankSettings-->>User: Current ranges and version history
    end
  • Element cards carry compact trend severity styling and a short signal so dangerous drift is visible without crowding the card.
  • Detailed trend data is loaded only when the chart icon is clicked.
  • Trend modals show the active tank target range as a chart band or line. Editing is handled from the Tank Settings target range page so ranges are managed at the tank level.
  • Future projection and advanced dose-response analytics are pro-level features.
  • Test detail responses can include lightweight trendAssessment metadata so cards can stand out immediately.
  • The default target range comes from the selected dosing method's correction reference data, currently Moonshiners formula targets. Tank custom ranges are stored as versioned active/inactive rows per tank/element in env-element-target-override.

Subscription And Free Access Flow

sequenceDiagram
    participant UI as User profile / Admin page
    participant API as REST API Gateway
    participant Checkout as user-billing-checkout
    participant Portal as user-billing-portal
    participant Friend as user-billing-friend-access
    participant Stripe as Stripe
    participant Webhook as user-billing-webhook
    participant Cognito as Cognito post-confirmation
    participant UserTable as DynamoDB env-user
    participant GrantTable as DynamoDB env-friend-access-grant

    UI->>API: POST /user/billing/checkout
    API->>Checkout: invoke
    Checkout->>UserTable: Read Stripe customer, pending checkout, and subscription state
    alt Open pending checkout session exists
        Checkout->>Stripe: Retrieve checkout session
        Checkout-->>UI: Reuse Checkout URL
    else Active Stripe subscription exists
        Checkout->>Stripe: Create billing portal session
        Checkout-->>UI: Portal URL
    else New paid subscription
        Checkout->>Stripe: Create checkout session with idempotency key
        Checkout->>UserTable: Mark checkout_pending / Stripe customer/session
        Checkout-->>UI: Checkout URL
    end

    Stripe->>API: POST /user/billing/webhook
    API->>Webhook: invoke
    Webhook->>UserTable: Store subscription id, status, tier, price id, clear pending checkout

    UI->>API: POST /user/billing/portal
    API->>Portal: invoke
    Portal->>Stripe: Create billing portal session
    Portal-->>UI: Portal URL

    UI->>API: POST /user/billing/friend-access
    API->>Friend: invoke as admin
    Friend->>GrantTable: Save email grant for this environment
    alt User already exists
        Friend->>UserTable: Set billing_exempt=true, access_tier=PRO
    else User signs up later
        Cognito->>GrantTable: Check email grant
        Cognito->>UserTable: Create user as PRO / billing exempt
    end
  • Free/friend access is account data, not a GitHub secret or build variable, and grants Pro-level access without Stripe billing.
  • Friend grants are environment-scoped because the table name includes the environment.
  • Granting friend access does not grant admin permissions.
  • Admin-only API access is reserved for the built-in bootstrap admin account.
  • Production Cognito verification email uses the SES-backed Reef.A.Matic <no-reply@reefamatic.com> sender. The SES domain identity, DKIM records, and custom MAIL FROM records are managed by the prod Terraform state because reefamatic.com is shared across environments.

UI Components To Endpoint Map

UI surfacePrimary componentsEndpoints called
App shellCustomAppLayout, DashboardSideNavigationPOST /user, POST /test/history/list, POST /dose/history/list, POST /program/history/list, websocket connect; the authenticated shell is Tailwind-first with a custom Reef.A.Matic side rail, top page header, profile action, and right-side element drawer while legacy route surfaces continue using existing local components during migration
Home dashboardHomePOST /test/history/list, POST /dose/history/list, POST /dose/elements/list, POST /test/elements/list, POST /test/history/icp/trend, POST /user/dashboard; loads the Reef Review summary first, renders 90-day default/custom trend cards only after the user requests Element Trends, uses the tank-method dose catalog for the default dashboard, uses selected test references and up to two selected dosing elements per custom chart, overlays the tank's Moonshiners/custom target range on each measured chart, opens raw measured/dosed values in a modal, and links those values to the underlying history records
Dashboard editorDashboardEditor, DraggableElementDrawerPOST /test/elements/list, POST /dose/elements/list, POST /user/dashboard, POST /user/dashboard/save, POST /user/dashboard/delete; starts with a blank canvas, lets users drag tests from the right drawer, lets each chart card select up to two tank-method dosing elements to compare against the measured test, saves named dashboards scoped to user and tank, preserves legacy saved dose-element selections, and enforces one saved dashboard for Basic users through the backend
Template editorTemplateEditor, DraggableElementDrawerPOST /dose/elements/list, POST /test/elements/list, POST /user/template, POST /user/template/save, POST /user/template/delete; starts with a blank canvas, remounts from the current router path when switching between test/dose/program templates, opens a type-specific right drawer, lets users create named dose/test/program entry templates from the tank-method dose catalog, and stores templates scoped to user and tank
Upload modalICPUploadPOST /test, then direct PUT to signed S3 URL
Tank settingsTankTargetRangesPOST /user/target-ranges; lists active Moonshiners/default or custom tank target ranges, saves custom ranges as new active versions, closes prior active rows, resets elements to method defaults, and shows per-element version history
ICP detailICPTest, ParameterWidgetPOST /test/history/icp, POST /test/history/icp/trend, POST /corrections; element trend modals show active tank target ranges and link to Tank Settings for edits
Reef ReviewHome, ReefReviewList, ReefReviewPage, DashboardSideNavigation, ICPTest, TestingPOST /user/reef-reviews; lazy-loads review summaries on the dashboard and review pages instead of blocking the global shell, uses /reef-review as the command-center entry route that opens the highest-priority review when one exists or shows a structured empty command-center state when none exist, links ICP and manual test results to their guided review, and displays a focused Tailwind command-center review with source context, corrections, daily adjustments, decisions, element attention, follow-up outcomes, and links back to the source test and generated draft program when applicable
Manual test detailTestingPOST /test/elements/list, POST /test/history, POST /test/history/save, POST /user/template; applies tank-scoped entry templates so common element sets can be reused without dragging each element individually
Dose historyDosingPOST /dose/elements/list, POST /dose/history, POST /dose/history/list, POST /dose/history/save, POST /dose/history/delete, POST /user/template; loads the selected tank method's dose catalog, applies tank-scoped entry templates, and supports multi-day dose counts that fan out into one dose-history save per date
Program historyProgramPOST /dose/elements/list, POST /program/history, POST /program/history/list, POST /program/history/save, POST /program/history/delete, POST /user/template; loads the selected tank method's dose catalog and applies tank-scoped program templates so common dosing schedules can be reused without dragging each element individually
User profile / billingUserProfilePOST /user, POST /user/save, POST /user/billing/checkout, POST /user/billing/portal, POST /user/billing/friend-access
Admin operations consoleAdminConfig, AdminOverview, AppErrorSummaryPOST /user/admin/overview, POST /user/admin/costs, POST /user/admin/config, POST /user/admin/config/save, POST /user/admin/errors, POST /user/admin/errors/clear, POST /user/billing/friend-access; shows deployed version/build metadata/release notes, all users with nested tank details, live login status from active WebSocket connections, usage patterns, AWS Cost Explorer service spend, Module tag spend, per-PDF cost attribution, RaM draft suggestion feedback quality, friend Pro access grants, config, recent support errors, and error acknowledgement actions
Realtime nav badgesWebSocketManager, DashboardSideNavigationWebSocket $connect / $disconnect, plus notifier pushes from DynamoDB streams

REST Endpoint To Lambda Map

EndpointLambda
POST /useruser-get-user-profile
POST /user/saveuser-save-user-profile
POST /user/billing/checkoutuser-billing-checkout
POST /user/billing/portaluser-billing-portal
POST /user/billing/friend-accessuser-billing-friend-access
POST /user/billing/webhookuser-billing-webhook
POST /user/admin/configuser-admin-config-get
POST /user/admin/config/saveuser-admin-config-save
POST /user/admin/errorsuser-admin-errors-list
POST /user/admin/errors/clearuser-admin-errors-clear
POST /user/admin/overviewuser-admin-overview-get
POST /user/admin/costsuser-admin-costs-get
POST /user/dashboarduser-dashboard-list
POST /user/dashboard/saveuser-dashboard-save
POST /user/dashboard/deleteuser-dashboard-delete
POST /user/templateuser-entry-template-list
POST /user/template/saveuser-entry-template-save
POST /user/template/deleteuser-entry-template-delete
POST /user/target-rangesuser-target-ranges
POST /user/reef-reviewsuser-reef-reviews
POST /testupload-presigned
POST /test/elements/listtest-list-test-elements
POST /test/historytest-get-test-history
POST /test/history/icptest-get-icp-test-history
POST /test/history/icp/trendtest-get-icp-element-trend
POST /test/history/listtest-list-test-history
POST /test/history/savetest-save-test-history
POST /correctionscorrections-apply-correction
POST /dose/elements/listdose-list-dose-elements
POST /dose/historyget-dose-history
POST /dose/history/listdose-list-dose-history
POST /dose/history/savedose-save-dose-history-apigw
POST /dose/history/deletedose-delete-dose-history
POST /program/historydose-get-program-history
POST /program/history/listdose-list-program-history
POST /program/history/savedose-save-program-history
POST /program/history/deletedose-delete-program-history

POST /test/elements/list returns the selectable tank test catalog for manual tests, templates, and custom dashboards. The seed table also stores RODI panel rows for ICP rendering, so the list Lambda filters to source=tank and defensively deduplicates by normalized element name before returning picker options.

Element trend charts match related tested and dosed names through canonical element groups. For example, Manganese charts include Manganese and Manganese-X doses, Iron charts include Iron and Iron-X doses, and Phosphorus charts include Phosphate dosing. Chart lines render each series from its own measured or dosed points so unrelated dates do not break the other line, and the X axis uses numeric timestamps so sparse measured points and dense dosing points align by actual date rather than array position.

Async Components

S3

BucketPurpose
env-reef-a-matic-uiStatic UI hosting origin behind CloudFront
env-reef-a-matic-apiLambda artifact/bootstrap bucket
env-reef-a-matic-uploadUploaded ICP PDFs; S3 notification target for upload-convert
dev-reef-a-matic-apiShared Terraform backend bucket; environment state is separated by key

Stripe

ComponentPurpose
stripe/ TerraformDefines Basic and Pro products/prices in the selected Stripe workspace
Checkout sessionsNew paid subscription start flow from user profile; backend reuses open pending sessions, redirects existing subscribers to the portal, and uses Stripe idempotency keys to prevent duplicate subscription starts
Billing portal sessionsCustomer self-service management flow from user profile
Webhook endpointUpdates env-user with subscription status, tier, price id, and Stripe ids

SQS

QueueProducerConsumerPurpose
env-upload-results.fifoupload-convert after extraction validation passescorrections-eval-element-test-resultPer-element ICP evaluation and categorization
env-icp-correction.fifocorrections-apply-correction, generate-past-program-historysave-dose-history-sqsTurn recommended corrections into dose-history writes
env-dose-save-daily-dosedose-apply-daily-dose after reading active env-program-history recordsdose-save-daily-doseSchedule/apply recurring daily dose records
env-dose-generate-past-dosedose-save-program-historygenerate-past-program-historyBackfill retrospective dose history from programs

EventBridge

RuleTargetPurpose
apply-daily-dosedose-apply-daily-doseRuns once per day at 01:00 UTC, reads active program-history records, filters elements by selected day of week, and pushes due entries into the daily-dose flow

WebSocket

ComponentPurpose
API Gateway WebSocket APIBrowser connects with Cognito token query param
ws-test-history-connect / ws-test-history-disconnectManage connection lifecycle
ws-test-history-notifyPush test-history stream updates to clients
ws-dose-history-notifyPush dose-history stream updates to clients
ws-program-history-notifyPush program-history stream updates to clients
env-websocket-connectionsStores active websocket connection ids

DynamoDB Tables

TableRole
env-userUser profile, access tier, Stripe customer ids, billing state, friend/free access state
env-user-tankTank profile per user
env-app-configAdmin-managed runtime settings, including Basic upload window minutes and Bedrock prompt templates such as bedrock_prompt_reef_review_interpretation
env-app-errorPersisted system error records keyed by support reference id, including operation, user id, tank id, request ids, exception summary, and stack trace
env-usage-eventAdmin cost attribution and metering events by user, tank, resource type, quantity, estimated USD cost, correlation id, and timestamp
env-friend-access-grantAdmin-created free-access email grants, applied immediately or at signup/profile load
env-user-dashboardUser-defined dashboard names, legacy selected element ids, and custom chart definitions keyed by dashboard id with a user_id_tank_id-index for tank-scoped dashboard lookup; each custom chart stores one test element id and up to two dosing element ids; legacy dose-element ids can still render for dashboards saved before the test-selection flow; Basic users can save one per tank, Pro users can save unlimited dashboards
env-user-entry-templateUser-defined manual entry templates for Test, Dose, and Program pages, keyed by template id with a user_id_tank_id_template_type-index for tank/type-scoped lookup
env-test-elementTest element metadata/catalog
env-test-historyManual test history and ICP history, plus ICP element lists and recommendations
env-reef-reviewReef Review records keyed by review id with user/tank, test-history, and draft-program indexes. ICP completion generates one review per test with status, severity, comparison context, triage findings, recommended actions, generated draft-program links, applied correction progress, and user decisions
env-batch-historyBatch-oriented test history support data
env-element-correctionCorrection formulas, categories, comments, targets
env-element-target-overrideVersioned tank-level custom target ranges for ICP trend charts. Active rows override the method default, inactive rows preserve range history, and reset closes the active row so Moonshiners/default guidance is used again
env-dose-elementDose element metadata/catalog
env-dose-historySaved dose-history records
env-program-historySaved dosing programs, including active/draft/inactive
env-daily-doseLegacy daily-dose projection table; scheduled dosing now derives due entries from active env-program-history records
env-program-draft-feedbackHuman-in-the-loop feedback captured when a user changes RaM draft program suggestions before activation
env-tank-dose-historyTank/date dose aggregation
env-tank-dose-elementsTank/date/element dose aggregation
env-websocket-connectionsActive websocket connections, including user id and connection timestamp for admin live login status

Notes

  • The UI reads almost everything through CustomAppLayout first, then individual route components call deeper detail endpoints.
  • The authenticated shell and Reef Review 2.0 workflow use Tailwind utility classes for the custom application frame and command-center review UI. Cloudscape theme tokens remain available for older route components until those surfaces are migrated.
  • System error handling:
  • REST Lambdas use a shared support-reference response for unexpected 500s
  • each generated reference id is logged to CloudWatch and persisted to env-app-error
  • persisted error records include operation, user id, tank id, API/Lambda request ids, exception summary, and stack trace when available
  • the Admin page displays recent error summaries from POST /user/admin/errors
  • admins can acknowledge errors through POST /user/admin/errors/clear, which deletes selected support-reference rows from env-app-error
  • async ICP conversion errors also write the reference id and friendly message to the affected env-test-history record
  • Admin usage and cost visibility:
  • POST /user/admin/overview aggregates env-user, env-user-tank, history tables, env-app-error, and env-usage-event
  • POST /user/admin/costs calls AWS Cost Explorer for actual spend grouped by AWS service and by the active Module cost allocation tag while filtering by the active Environment tag
  • the Admin Usage & Costs tab combines Cost Explorer actuals with RaM per-PDF attribution so a single ICP upload can be reviewed by service cost split
  • existing history records provide immediate user/tank activity counts
  • env-usage-event provides durable per-user/per-tank/per-PDF cost attribution as workflows emit metering events with a correlation id, while older PDFs fall back to calculated estimates from their stored records
  • Admin HITL suggestion feedback:
  • POST /user/admin/overview also reads env-program-draft-feedback
  • the Admin page's RaM Feedback tab summarizes suggestion-vs-activated drift by amount, selected days, weighted weekly exposure, element, user, and recency
  • the same tab compares activated draft feedback to the next follow-on ICP result when available, including elapsed days, target-distance before/after, and average outcome improvement
  • large draft edits are highlighted so repeated misses can be reviewed before they become a support pattern
  • ICP uploads are the most asynchronous part of the system:
  • REST creates the signed upload URL
  • S3 triggers conversion
  • conversion uses Bedrock for extraction, then deterministically overrides canonical fields such as Analysis No, sample date, tank measurements, and Osmose/RO measurements from PDF text before writing the history row
  • conversion writes the initial history row directly and validates required metadata, duplicate element names, and the validation_required=true source=tank panel from env-test-element before fan-out
  • Osmose/RO measurements are stored as separate ICP result cards, for example Copper (RO), in the Osmose category and are not sent to correction/draft-program evaluation
  • missing expected elements are added as status=MISSING placeholders in their normal ICP category so the user can investigate
  • structurally invalid extraction, such as missing metadata or duplicate element names, is saved as ERROR and never reaches corrections or draft-program generation
  • correction and websocket updates converge back into env-test-history
  • upload placeholders and websocket updates keep the UI from relying on polling for final state
  • Test reference data deploy:
  • when the test module changes, the deploy workflow refreshes the Oceamo element seed JSON in reef-a-matic-seed-data
  • the workflow also refreshes the Triton ICP element seed JSON so Triton uploads can use a Triton-branded reference panel without enabling Triton as a user profile dosing method
  • the workflow invokes env-test-load-test-data for each test seed so env-test-element carries current validation_required flags before new uploads depend on them
  • Dose and program history also have async behavior:
  • saving a program can enqueue backfill work
  • inactive-program backfill caps generated dose-history records at the current date and honors each element's selected weekdays
  • active programs with a start date on or before today keep their open-ended active record, schedule future daily dosing, and enqueue a bounded backfill through today
  • editing the current active program versions the record instead of updating it in place: the existing active program is closed as inactive, the edited values are saved as a new active program, and historical generated dose records are not rewritten
  • applying ICP corrections can enqueue dose-history writes
  • EventBridge creates daily dose-history records automatically by reading active program records and applying the selected weekdays for each element
  • Manual dose entry can create several dated dose-history records from one screen when an element has a multi-day dose count. New saves merge with existing dose-history records on those dates instead of replacing unrelated elements.
  • Manual test, dose, and program templates are stored separately from history records and scoped by user, tank, and template type.
  • Draft dosing programs can be created from ICP processing and can be activated later, moving the prior active program to inactive status. Pro draft generation is centralized in the shared DailyDoseDraftProgramService, which is called by both corrections-eval-element-test-result during upload processing and test-get-icp-test-history during ICP detail reads. It uses AI daily-dose suggestions with deterministic tank-response guardrails based on the user's own active program snapshot, selected dosing days, ICP history, dose history, prior draft outcomes, measured values, target ranges, and lab guidance. Drafts preserve nonzero active-program elements and actionable suggested changes, but first-ICP drafts without an active baseline do not include zero-dose placeholder elements. Drafts can adjust either the dose amount or the checked days of the week, such as keeping the same amount while reducing frequency. When a user edits a generated draft before activation, RaM stores the original suggestion, activated value, and day-of-week changes as human-in-the-loop feedback. Future draft suggestions can include prior outcome context that compares the source ICP result to the next ICP after activation so RaM can favor adjustments that moved the tank closer to target over an appropriate time window.
  • Reef Review generation runs after ICP evaluation completes and after manual test saves that produce review-worthy results. corrections-eval-element-test-result creates or updates one env-reef-review row for the completed ICP, compares it against the default prior-test scope, stores findings/actions, preserves existing user decision state on retries, links the generated Pro draft program when one exists, and creates per-element daily adjustment actions from draft-program suggestion metadata. When older/restored or first-draft program elements do not carry explicit suggestion metadata, Reef Review derives the current daily value from the latest non-ICP-correction dose history on or before the source test date and uses the draft program value as the suggestion so the daily adjustment rail still has element-level rows. Finding severity always checks absolute measured values against target ranges before trend stability; hazardous fallback thresholds, such as Copper, also provide target bounds when the report/reference row lacks one. test-save-test-history creates lightweight manual-test reviews when tank target ranges, statuses, or trends need attention; these guide users back to the source test and recent dosing/program context without creating correction checklists or draft programs. Bedrock interpretation only rewrites human-readable ICP review, finding, and action text from deterministic facts; the prompt template is read from env-app-config and auto-seeded with a default if missing. Applying ICP corrections updates one-time action progress, including partial completion for multi-day corrections, and review reads/recalculations reconcile correction progress only from ICP-correction dose rows dated on or after the source test date so older ICP corrections cannot make a newer review appear partially applied. Daily adjustment counts include only draft-program rows where amount, unit, or selected dosing days actually changed. Activating a generated draft program marks the draft action applied while recording accepted or overridden values, and review-level completed/dismissed timestamps separate active queues from history. Initial ICP review generation and Reef Review RECALCULATE both support prior-test trend history for Last 3, Last 5, Last 10, and All compare scopes by selecting from the chosen lookback and regenerating comparison-derived findings/actions while preserving action state. Reef Review detail reads also compute follow-up outcomes from the next completed test for the tank so users can see whether findings moved closer to target without persisting derived rows. The UI exposes a dashboard review summary, a single Reef Reviews nav entry, a /reef-review command-center entry point, and detail routes through /reef-review/:id; review summaries are loaded by the dashboard/inbox surfaces instead of the global layout, and dashboard Element Trends are loaded on demand, so initial page load does not fetch review metadata or trend details for unrelated work.
  • Subscription behavior gates uploads and predictive features:
  • new account creation starts with an explicit Free Trial, Basic, or Pro selection
  • Free Trial accounts use a 7-day app trial that does not require Basic or Pro checkout
  • app trial users receive Pro functionality and 3 total ICP PDF uploads until the trial expires
  • upload requests run an allowance preflight before any S3 PUT begins so trial/basic entitlement failures do not partially upload a selected batch
  • paid subscriptions use Stripe recurring monthly or annual prices and auto-renew until canceled
  • paid subscriptions receive a 30-day free Pro trial with payment collected up front and billing starting after the trial
  • launch promotion code RAM_2026 applies 50% off the selected monthly or annual subscription price for the first year
  • basic: one upload per configured window
  • basic: one saved custom dashboard per tank
  • pro: unlimited uploads, unlimited saved custom dashboards, future projections, trend warnings, dose-response analytics, and draft program automation
  • friend/free access: grants Pro access, bypasses Stripe billing, and is stored on the user record
  • canceled, cancel-at-period-end, or inactive Stripe subscriptions retain user, tank, and history records but resolve to no effective app access until billing is restored
  • Realtime updates shown in the left navigation are driven by DynamoDB streams into websocket notifier Lambdas, not by polling. Test, dose, and program history streams use new-and-old images so delete events can include the record identity needed for websocket removal messages.
  • The app shell applies dose/program websocket menu payloads directly and keeps a short refresh fallback for eventual consistency during async queue fan-out.
  • The Admin Users view reads active WebSocket connection rows to show which users are currently logged in, with the UI refreshing the overview periodically.
  • Dev and prod are isolated by branch, Terraform state key, resource names, Cognito user pools, API Gateway stages, S3 buckets, CloudFront aliases, and Stripe workspace/key usage. Shared domain email infrastructure is owned by prod Terraform, while each Cognito pool uses the verified SES identity so verification messages come from Reef.A.Matic <no-reply@reefamatic.com>.