shelf — cross-layer dependency map
reads
writes
triggers
model call
external
Webhooks — Shopify pushes to us
📨 inbound — Shopify initiates
app/uninstalled
Merchant removes the app from their store. We delete their merchant row and all dependent data (products, competitors, briefings) via cascade.
Handler: webhooks.tsx → deleteMerchantAndSchedules()
shop/redact
GDPR: Shopify asks us to permanently delete all data for a shop. Same as uninstall — full merchant + dependents deletion.
Handler: webhooks.tsx → deleteMerchantAndSchedules()
customers/data_request
GDPR: A customer asked Shopify what data an app holds on them. We respond that we store no customer PII — only aggregate order counts.
Handler: webhooks.tsx → returns 200 + note
customers/redact
GDPR: Delete all data for a specific customer. We have no customer-level data to delete — acknowledged with 200.
Handler: webhooks.tsx → returns 200
app_subscriptions/update
A billing subscription changed — activated, cancelled, or frozen. We update the merchant's plan and paywall status accordingly.
Handler: webhooks.tsx → updateMerchantPlan() + updatePaywallStatus()
GraphQL — we pull from Shopify
→ outbound — we initiate
products.list
Fetch the merchant's full product catalogue so they can pick which SKUs to track during onboarding.
Called by: /app/onboarding/* · scope: read_products
SignalS1Orders — realized prices
Orders from last 30 days. Calculates the average realized unit price per product after discounts are applied.
Called by: afterAuth → syncMerchantShopifySignals() · scope: read_orders
SignalS2Orders — discount depth
Orders from last 30 days. Calculates what % of order value is discounted and whether discounting is structural or promotional.
Called by: afterAuth → syncMerchantShopifySignals() · scope: read_orders
SignalS3Orders — refund rate
Orders from last 30 days. Counts how many had refunds — a proxy for product-market fit and quality issues.
Called by: afterAuth → syncMerchantShopifySignals() · scope: read_orders
SignalS5Orders — UTM channel split
Orders from last 30 days. Breaks down where orders came from: paid social, organic, email, direct.
Called by: afterAuth → syncMerchantShopifySignals() · scope: read_orders
SignalS6Orders — new vs returning
Orders from last 30 days. Ratio of first-time buyers to returning customers.
Called by: afterAuth → syncMerchantShopifySignals() · scope: read_orders
HeroOrders — hero product inference
Orders from last 30 days. Identifies the highest-revenue product among the merchant's confirmed matched SKUs.
Called by: Phase 6 (post-onboarding) · scope: read_orders
Billing API — create + verify charge
Creates a recurring charge when a merchant upgrades. Verifies the charge status when Shopify redirects back after the merchant approves or declines.
Called by: /app/billing + /app/billing/callback
Traffic & Compute
ALB — shelf-alb
Public HTTPS entry point. Terminates TLS, routes all traffic to the Remix container on port 3000. HTTP → 301 HTTPS redirect on port 80.
Domain: app.shelfplugin.com · Target group: shelf-remix-tg · Cert: ACM wildcard *.shelfplugin.com
→ hits: ECS shelf-remix-service (all routes)
ECS Fargate — shelf-remix-service
Long-running service that serves the Remix app. Always on — handles every merchant request, webhook, and OAuth flow. Pulls image from ECR on deploy.
Task def: shelf-remix-task · Image: ECR shelf-remix · SG: shelf-remix-sg
→ serves: /app, /webhooks, /auth/*, /api/*, /app/billing, /app/settings/*
→ reads secrets from: Secrets Manager (DATABASE_URL, SHOPIFY_API_SECRET)
→ logs to: CloudWatch /ecs/shelf-remix
ECS Fargate — shelf-crawl-task
On-demand task, not a service. Spun up per crawl job via ECS.RunTask() — runs the Python pipeline, writes the briefing, then exits. Cold start ~30s.
Task def: shelf-crawl-task · Image: ECR shelf-crawl · SG: shelf-crawl-sg (no inbound)
→ triggered by: pg-boss job dequeue via boss.server
→ reads secrets from: Secrets Manager (DATABASE_URL, ANTHROPIC_API_KEY, LANGFUSE_*)
→ writes to: RDS briefings + competitors tables
→ logs to: CloudWatch /ecs/shelf-crawl
ECS Fargate — shelf-schema-runner
One-shot migration task. Runs db/schema.sql on deploy when schema changes are needed, then exits. Quiet the rest of the time.
→ logs to: CloudWatch /ecs/shelf-schema-runner
Data & Storage
RDS PostgreSQL — shelf-db
The only persistent store. Holds all 5 app tables plus pg-boss job queue. Private subnet — no public access. Both ECS tasks connect via DATABASE_URL from Secrets Manager.
Engine: PostgreSQL 15 · Instance: db.t4g.micro · Endpoint: shelf-db.c0f8siq66jr4.us-east-1.rds.amazonaws.com
→ tables: merchants, products, competitors, briefings, crawl_jobs (pg-boss)
→ read by: shelf-remix-service (all loaders), shelf-crawl-task (pipeline)
→ written by: shelf-remix-service (actions), shelf-crawl-task (briefings + competitor enrichment)
ECR — shelf-remix + shelf-crawl
Docker image registry. GitHub Actions builds and pushes both images on every main branch deploy. ECS pulls the latest image tag when launching tasks.
shelf-remix: 426018613230.dkr.ecr.us-east-1.amazonaws.com/shelf-remix
shelf-crawl: 426018613230.dkr.ecr.us-east-1.amazonaws.com/shelf-crawl
→ pushed by: GitHub Actions (shelf-github-deploy role)
→ pulled by: ECS task launch (shelf-ecs-execution-role)
S3 + CloudFront — marketing site
Static site hosting for shelfplugin.com. Separate from the app — different repo, different deploy role. CloudFront sits in front for HTTPS + caching.
Bucket: shelfplugin-site · CF distribution: E325D94Y9QQSAU
→ deployed by: jokeane9/shelf-site repo (shelf-site-deploy role)
Security & Config
Secrets Manager
Runtime secret injection. ECS tasks declare secrets in their task definition — the execution role fetches them at container start and injects as env vars. Nothing hardcoded in images.
shelf/DATABASE_URL → both ECS tasks
shelf/SHOPIFY_API_SECRET → shelf-remix-task only
shelf/ANTHROPIC_API_KEY → shelf-crawl-task only
shelf/LANGFUSE_* (3 keys) → shelf-crawl-task only
IAM Roles
Four roles, each scoped to minimum permissions. Execution role fetches secrets + pulls ECR. Task role can trigger crawl ECS tasks. Deploy role can push images and update services.
shelf-ecs-execution-role → ECR pull + Secrets Manager get (scoped to shelf/*)
shelf-ecs-task-role → ECS RunTask on shelf-crawl-task + iam:PassRole
shelf-github-deploy → ECR push + ECS deploy + iam:PassRole (OIDC, main branch only)
shelf-site-deploy → S3 sync + CloudFront invalidation only
Security Groups — traffic rules
Four SGs forming a layered perimeter. Internet hits ALB only. ALB → Remix only. Remix + Crawl → RDS only. Crawl has no inbound — outbound-only for web crawling.
shelf-alb-sg → TCP 443/80 from 0.0.0.0/0
shelf-remix-sg → TCP 3000 from shelf-alb-sg only
shelf-crawl-sg → no inbound (outbound: Anthropic API, competitor URLs, RDS)
shelf-rds-sg → TCP 5432 from shelf-remix-sg + shelf-crawl-sg only
Observability
CloudWatch Logs
Three log groups — one per ECS task. The Remix log is your production debugging surface: auth flows, webhook handler output, loader errors. Crawl log shows pipeline progress and Layer 4 output.
/ecs/shelf-remix → [afterAuth], [webhook], loader errors, route logs
/ecs/shelf-crawl → Layer 0–4 pipeline, Langfuse traces, crawl failures
/ecs/shelf-schema-runner → migration output only
Langfuse — Layer 4 traces
Captures every Claude API call from the crawl pipeline: full prompt + response, token counts, latency, merchant ID, model version. The dataset for evaluating prompt quality over time.
Injected via: LANGFUSE_PUBLIC_KEY + LANGFUSE_SECRET_KEY + LANGFUSE_BASE_URL (Secrets Manager)
→ called by: shelf-crawl-task at Layer 4 only
Deploy Pipeline
GitHub Actions → ECR → ECS
Push to main triggers the deploy workflow. Builds both Docker images, pushes to ECR, then updates the ECS service to pull the new image. Zero-downtime rolling deploy via ECS service update.
1. git push main → GitHub Actions fires
2. docker build shelf-remix + shelf-crawl
3. ECR push (shelf-github-deploy OIDC role)
4. ECS service update → shelf-remix-service pulls new image
5. shelf-crawl-task picks up new image on next ECS.RunTask() call
DNS & TLS — Route 53 + ACM
Route 53 hosts the shelfplugin.com zone. ACM wildcard cert covers both root and all subdomains. ALB does TLS termination — containers only see plain HTTP on port 3000.
app.shelfplugin.com → CNAME → shelf-alb DNS
shelfplugin.com → ALIAS → CloudFront (marketing site)
Cert: ACM *.shelfplugin.com (us-east-1, auto-renews)
🖼 UI Components
BriefingCard
card renderer
BriefingSection
briefing layout wrapper
MarketReadSection
market summary
StateIndicatorBar
crawl status · freshness
🗺 Routes
/app
app._index.tsx
LAUI
⚡ user
/app/onboarding/*
6 step routes
LAUI
⚡ user
/app/billing
app.billing.tsx
LAUI
⚡ user
/api/sidekick
api.sidekick.tsx
L
⚡ Sidekick calls
/api/crawl-status
api.crawl-status.tsx
L
↺ polled ~2s
/webhooks
webhooks.tsx
A
📨 inbound
/app/settings/*
3 settings routes
LAUI
⚡ user
/app/billing/callback
app.billing.callback.tsx
L
⚡ Shopify redirect
/api/sidekick/*
market · peers · comparisons
L
⚡ Sidekick calls
/auth/*
auth.*.tsx
LA
⚡ OAuth redirect
📦 Lib / Models
session.server
Shopify auth · token
db.server
pg pool · query wrapper
boss.server
pg-boss client
types.ts
Merchant · Briefing · Plan
shopify-billing.server
plan helpers
🗄 Database
merchants
session · plan · config
products
tracked SKUs
competitors
URLs · homepage extras
briefings
AI output · per cycle
crawl_jobs
pg-boss queue
🤖 AI Layer
crawl pipeline
Python · ECS task
Layer 4 prompt
crawl/prompts/
claude-3-5-sonnet
anthropic model
briefing schema
JSON output shape
🌐 External
Shopify Admin API
product catalogue
Shopify Billing API
subscriptions
Shopify OAuth
app install · token
Shopify Webhooks
inbound events
Anthropic API
claude-3-5-sonnet
Playwright / web
headless crawl
← Hover any node
to inspect it