Chapter 6: System Architecture

This chapter defines the high-level architecture of AdPriority, covering component decomposition, deployment topology, security boundaries, and communication patterns. Every design decision maps back to the constraints established in Part I: a single-developer build targeting the Shopify App Store, deployed on existing Synology NAS infrastructure.


4.1 High-Level Architecture

The system is composed of five primary subsystems connected by REST APIs, webhooks, and scheduled jobs. The following diagram traces the complete path from a merchant’s Shopify store through to a live Google Ads campaign.

                           MERCHANT BROWSER
                          (Shopify Admin iFrame)
                                  |
                                  | App Bridge 4.x
                                  | Session Token
                                  v
 +-----------------------------------------------------------------+
 |                      AdPriority Application                     |
 |                                                                 |
 |  +---------------------+        +----------------------------+ |
 |  |   Frontend (SPA)    |  REST  |      Backend API           | |
 |  |                     | -----> |                            | |
 |  |  React 18 + Vite    |  JSON  |  Express.js + TypeScript   | |
 |  |  Polaris v13        |        |  Prisma ORM                | |
 |  |  React Query        |        |  Zod Validation            | |
 |  +---------------------+        +---+--------+--------+------+ |
 |                                     |        |        |        |
 |                              +------+   +----+----+   |        |
 |                              |          |         |   |        |
 |                              v          v         v   v        |
 |                      +----------+ +----------+ +----------+   |
 |                      |PostgreSQL| |  Worker  | |  Redis   |   |
 |                      |    16    | | (Bull    | | (job     |   |
 |                      |adpriority| |  queue)  | |  store)  |   |
 |                      |   _db    | +----+-----+ +----+-----+   |
 |                      +----------+      |            |          |
 +-----------------------------------------------------------------+
        |              |                  |            |
        |              |                  |            |
        v              v                  v            v
 +-----------+  +-----------+  +------------------+  +----------+
 |  Shopify  |  |  Shopify  |  | Google Sheets    |  |  Google  |
 |  Admin    |  | Webhooks  |  | API (v4)         |  |   Ads    |
 |  API      |  |           |  |                  |  |   API    |
 | (GraphQL) |  | products/ |  | Writes priority  |  | (future) |
 |           |  | update    |  | data to Sheet    |  |          |
 +-----------+  | app/      |  +--------+---------+  +----------+
                | uninstall |           |
                +-----------+           v
                              +------------------+
                              | Google Merchant  |
                              | Center           |
                              |                  |
                              | Fetches Sheet    |
                              | daily as         |
                              | supplemental     |
                              | feed             |
                              +--------+---------+
                                       |
                                       v
                              +------------------+
                              | Custom Labels    |
                              | Applied to       |
                              | Product Listings |
                              |                  |
                              | label_0: priority|
                              | label_1: peak_szn|
                              | label_2: type    |
                              | label_3: status  |
                              | label_4: brand   |
                              +--------+---------+
                                       |
                                       v
                              +------------------+
                              | PMAX Campaigns   |
                              | Listing Groups   |
                              | filtered by      |
                              | custom_label_0   |
                              +------------------+

End-to-end latency budget: A priority change made in the AdPriority UI reaches a live Google Ads campaign within 24 hours (bounded by GMC’s daily supplemental feed fetch cycle).


4.2 Component Breakdown

4.2.1 Shopify Embedded App (Frontend)

The frontend runs as an embedded iFrame inside the Shopify Admin, using App Bridge 4.x for authentication and navigation.

AttributeValue
FrameworkReact 18
Build toolVite
UI libraryShopify Polaris v13
State mgmtReact Query (TanStack Query v5)
RoutingReact Router v6
ChartsChart.js (lightweight)
AuthShopify App Bridge session tokens
Bundle target< 200 KB gzipped

Key pages:

PageRoutePurpose
Dashboard/Priority distribution, sync status
Products/productsProduct list with inline priority edit
Rules/rulesRule builder and management
Calendar/calendarSeasonal priority matrix
Sync/syncSync status, history, manual trigger
Settings/settingsGoogle connection, defaults, billing

The frontend makes no direct calls to Google APIs. All external integrations are mediated by the backend.

4.2.2 Backend API (Express.js + TypeScript)

The backend serves three roles: API server for the frontend, webhook receiver for Shopify, and host for the inline worker and scheduler.

AttributeValue
RuntimeNode.js 20 LTS
FrameworkExpress.js 4.x
LanguageTypeScript 5.x (strict mode)
ORMPrisma 5.x
ValidationZod
LoggingPino (structured JSON)
Port3010 (configurable via PORT env)

Route groups:

/auth/shopify/*          Shopify OAuth install + callback
/auth/google/*           Google OAuth connect + callback
/api/v1/products/*       Product CRUD, bulk ops, import
/api/v1/rules/*          Rule CRUD, reorder, test, apply
/api/v1/seasons/*        Season CRUD, preview, transition
/api/v1/sync/*           Trigger, status, history, export
/api/v1/settings/*       Store config, label config
/api/v1/dashboard/*      Stats, activity feed
/webhooks/shopify/*      products/create, update, delete, app/uninstalled
/health                  Liveness probe

4.2.3 Database (PostgreSQL 16)

AdPriority uses the shared postgres16 container already running on the Synology NAS. A dedicated database and user provide logical isolation.

AttributeValue
Containerpostgres16 (PostgreSQL 16 Alpine)
Databaseadpriority_db
Useradpriority_user
Schemapublic
Internal port5432 (container-to-container)
External port5433 (NAS host)
Networkpostgres_default (shared bridge)
ORMPrisma (migrations, type-safe client)

Core tables (9 total):

tenants             Tenant registry (one row per Shopify shop)
products            Product priority scores and GMC mapping
rules               Priority rule definitions
rule_conditions     AND/OR conditions per rule
seasons             Season date ranges per store
season_rules        Category x Season priority matrix
sync_logs           GMC sync audit trail
subscriptions       Shopify billing records
audit_logs          Change tracking for compliance

See Chapter 12 for the complete schema definition and Prisma model.

4.2.4 Worker (Bull+Redis Job Queue)

Background jobs run in a dedicated worker process backed by Bull + Redis for reliable job queuing, retry semantics, and concurrency control (see ADR-010). The worker uses the same Docker image as the backend but runs a different entrypoint (npm run worker).

JobTriggerFrequency
Shopify product importManual / app installOn demand
Priority recalculationWebhook / rule changeEvent-driven
Google Sheet updatePriority change / cronHourly batch
Season transition checkCronDaily at 00:00 UTC
New arrival expiryCronDaily at 01:00 UTC
ReconciliationCronDaily at 03:00 UTC
Sync log cleanupCronWeekly (Sunday)

4.2.5 External Integrations

+---------------------+---------------------------+---------------------------+
| Integration         | MVP (Phase 0-2)           | SaaS (Phase 3+)           |
+---------------------+---------------------------+---------------------------+
| Shopify Admin API   | GraphQL for product fetch | Same                      |
| Shopify Webhooks    | products/*, app/*         | Same + orders/* (Pro)     |
| Google Sheets API   | Write priority data       | Same (AdPriority tier)    |
| GMC Content API     | Not used                  | Direct label updates      |
| Google Ads API      | Not used                  | Performance data (Pro)    |
| Shopify Billing API | Not used (Nexus only)     | Subscription management   |
+---------------------+---------------------------+---------------------------+

4.3 Deployment Architecture

4.3.1 Infrastructure Topology

AdPriority deploys as Docker containers on a Synology DS920+ NAS (192.168.1.26), joining the existing postgres_default network for database access. Cloudflare Tunnel provides a secure HTTPS endpoint for Shopify to reach the application without port forwarding or a static IP.

                    INTERNET
                       |
             +-------------------+
             | Cloudflare Edge   |
             | TLS termination   |
             | DDoS protection   |
             +--------+----------+
                      |
             Cloudflare Tunnel
             (encrypted tunnel)
                      |
  +-------------------------------------------------+
  |            Synology NAS (192.168.1.26)          |
  |                                                 |
  |  +-------------------------------------------+ |
  |  |        Docker Engine                       | |
  |  |                                            | |
  |  |  +----------------+   +----------------+  | |
  |  |  | adpriority     |   | postgres16     |  | |
  |  |  | (backend +     |   | (PostgreSQL 16)|  | |
  |  |  |  frontend +    |   |                |  | |
  |  |  |  worker)       |   | adpriority_db  |  | |
  |  |  |                |   | salessight_db  |  | |
  |  |  | Port: 3010     |   | shopsyncflow_db|  | |
  |  |  +-------+--------+   | stanly_db      |  | |
  |  |          |             | taskmanager_db |  | |
  |  |          |             +-------+--------+  | |
  |  |          |                     |           | |
  |  |          +----------+----------+           | |
  |  |                     |                      | |
  |  |            postgres_default                | |
  |  |            (bridge network)                | |
  |  |                                            | |
  |  |  +----------------+                       | |
  |  |  | cloudflared    |                       | |
  |  |  | (tunnel agent) |                       | |
  |  |  | Routes:        |                       | |
  |  |  | app.adpriority |                       | |
  |  |  |  .com -> :3010 |                       | |
  |  |  +----------------+                       | |
  |  +-------------------------------------------+ |
  +-------------------------------------------------+

4.3.2 Container Configuration

Development (docker-compose.yml):

services:
  adpriority:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3010:3010"
    volumes:
      - ./backend/src:/app/backend/src
      - ./admin-ui/src:/app/admin-ui/src
    environment:
      NODE_ENV: development
      DATABASE_URL: postgresql://adpriority_user:${DB_PASSWORD}@postgres16:5432/adpriority_db
      REDIS_URL: redis://redis:6379
      SHOPIFY_API_KEY: ${SHOPIFY_API_KEY}
      SHOPIFY_API_SECRET: ${SHOPIFY_API_SECRET}
      GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
      GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
      ENCRYPTION_KEY_V1: ${ENCRYPTION_KEY_V1}
    depends_on:
      - redis
    networks:
      - postgres_default
      - adpriority_internal

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    volumes:
      - redis-data:/data
    networks:
      - adpriority_internal

volumes:
  redis-data:

networks:
  postgres_default:
    external: true
  adpriority_internal:
    driver: bridge

Production (docker-compose.prod.yml):

services:
  adpriority:
    image: adpriority:${VERSION:-latest}
    restart: always
    environment:
      NODE_ENV: production
      DATABASE_URL: ${DATABASE_URL}
      ENCRYPTION_KEY_V1: ${ENCRYPTION_KEY_V1}
      REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
    networks:
      - postgres_default
      - adpriority_internal
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:3010/health/live"]
      interval: 30s
      timeout: 5s
      retries: 3
    deploy:
      resources:
        limits:
          memory: 512M

  worker:
    image: adpriority:${VERSION:-latest}
    restart: always
    command: ["npm", "run", "worker"]
    environment:
      NODE_ENV: production
      DATABASE_URL: ${DATABASE_URL}
      ENCRYPTION_KEY_V1: ${ENCRYPTION_KEY_V1}
      REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
    networks:
      - postgres_default
      - adpriority_internal
    deploy:
      resources:
        limits:
          memory: 512M

  redis:
    image: redis:7-alpine
    restart: always
    command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}", "--appendonly", "yes"]
    volumes:
      - redis-data:/data
    networks:
      - adpriority_internal
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3
    deploy:
      resources:
        limits:
          memory: 128M

volumes:
  redis-data:

networks:
  postgres_default:
    external: true
  adpriority_internal:
    driver: bridge

4.3.3 Container Strategy

The backend serves the compiled frontend as static files from the Express process. A separate worker container (same image, different entrypoint) processes Bull queue jobs. Redis provides the job queue backing store.

adpriority-backend container (Node.js 20)
|
+-- Express server (:3010)
|   +-- /api/v1/*          --> API routes
|   +-- /auth/*            --> Token Exchange routes
|   +-- /webhooks/*        --> Shopify webhooks
|   +-- /health            --> Liveness + readiness probes
|   +-- /*                 --> Static files (React build)
|
+-- Bull queue producers
|   +-- Enqueue jobs on webhook events, API actions, schedules
|
+-- Prisma Client
    +-- postgres16:5432/adpriority_db

adpriority-worker container (Node.js 20, same image)
|
+-- Bull queue consumers
|   +-- sync-sheet         --> Google Sheets batch sync
|   +-- priority-recalc   --> Rule/season-driven recalculation
|   +-- product-import     --> Shopify catalog import
|   +-- reconciliation     --> Daily Shopify/DB reconciliation
|   +-- cleanup            --> Sync log rotation
|
+-- Prisma Client
    +-- postgres16:5432/adpriority_db

adpriority-redis container (Redis 7 Alpine)
|
+-- Job queue persistence (AOF)
+-- Bull Board dashboard data

4.4 Security Architecture

4.4.1 Authentication Layers

+---------------------+-------------------+-------------------------------+
| Layer               | Mechanism         | Implementation                |
+---------------------+-------------------+-------------------------------+
| Shopify Merchant    | App Bridge        | Session token verification    |
| (embedded app)      | Session Token     | via Shopify library           |
+---------------------+-------------------+-------------------------------+
| Shopify API         | OAuth 2.0         | Access token stored           |
| (product data)      | Access Token      | encrypted in stores table     |
+---------------------+-------------------+-------------------------------+
| Google APIs         | OAuth 2.0         | Refresh token stored          |
| (Sheets, GMC, Ads)  | Refresh Token     | encrypted in stores table     |
+---------------------+-------------------+-------------------------------+
| Shopify Webhooks    | HMAC-SHA256       | Signature in                  |
| (event ingestion)   | Signature         | X-Shopify-Hmac-SHA256 header  |
+---------------------+-------------------+-------------------------------+
| Cloudflare Tunnel   | Tunnel Token      | Only Cloudflare can reach     |
| (network)           |                   | the backend; no open ports    |
+---------------------+-------------------+-------------------------------+

4.4.2 Credential Storage

All sensitive credentials are encrypted at rest using AES-256-GCM with a master key stored in versioned environment variables (ENCRYPTION_KEY_V1, ENCRYPTION_KEY_V2, etc.), never in the database or code repository. See ADR-011 for the versioned ciphertext format (v{n}:iv:tag:ciphertext).

CredentialStorage LocationEncryption
Shopify access tokentenants.shopify_access_tokenAES-256-GCM (v{n}:iv:tag:ciphertext)
Google refresh tokentenants.google_refresh_tokenAES-256-GCM (v{n}:iv:tag:ciphertext)
Shopify API secretEnvironment variableNot in DB
Google client secretEnvironment variableNot in DB
Database passwordEnvironment variableNot in DB
Encryption master keyEnvironment variableNot in DB

4.4.3 Data Protection

ConcernMitigation
SQL injectionPrisma parameterized queries (no raw SQL)
XSSReact auto-escaping + Polaris components
CSRFShopify session tokens (stateless, per-request)
Rate limitingExpress middleware: 100 req/min API, 10 req/min sync
Webhook spoofingHMAC-SHA256 verification on every webhook
Token leakageTokens never logged; Pino redaction filters
Network exposureCloudflare Tunnel (no open ports on NAS)
Content injectionCSP headers: frame-ancestors https://*.myshopify.com https://admin.shopify.com; script-src 'self' https://cdn.shopify.com
Multi-tenant data leakAll queries scoped by tenant_id (Prisma middleware)

4.4.4 Rate Limiting

+--------------------------+----------+---------+
| Endpoint Group           | Limit    | Window  |
+--------------------------+----------+---------+
| API (authenticated)      | 100 req  | 1 min   |
| Sync trigger             | 10 req   | 1 min   |
| Bulk operations          | 5 req    | 1 min   |
| Export / download        | 5 req    | 1 min   |
| Webhooks (Shopify-signed)| No limit | --      |
+--------------------------+----------+---------+

4.5 Communication Patterns

4.5.1 Frontend to Backend (REST API)

The React frontend communicates with the Express backend via JSON REST APIs. Authentication is handled by Shopify App Bridge session tokens, which are attached as Bearer tokens to every request.

Browser (iFrame)
    |
    |  GET /api/v1/products?priority=5&page=1
    |  Authorization: Bearer <session-token>
    |
    v
Express API
    |
    |  1. Verify session token (Shopify library)
    |  2. Extract tenant_id from token
    |  3. Query database (scoped by tenant_id)
    |  4. Return JSON response
    |
    v
Browser renders Polaris components

4.5.2 Shopify to AdPriority (Webhooks)

Shopify delivers event payloads over HTTPS to registered webhook endpoints. Cloudflare Tunnel ensures the backend is reachable from Shopify’s servers.

Shopify Platform
    |
    |  POST /webhooks/shopify/products/update
    |  X-Shopify-Hmac-SHA256: <signature>
    |  Content-Type: application/json
    |
    v
Express Webhook Handler
    |
    |  1. Verify HMAC signature
    |  2. Parse product payload
    |  3. Enqueue job to Bull queue (with dedup key)
    |  4. Return 200 OK immediately
    |
    v
Bull Worker (separate container)
    |
    |  1. Look up product in database
    |  2. Check for type/tag changes
    |  3. Recalculate priority (if not locked)
    |  4. Mark product as needs_sync
    |
    v
Database updated (async Sheet sync on next hourly batch)

Registered webhooks:

TopicEndpointPurpose
products/create/webhooks/shopify/products/createApply initial priority
products/update/webhooks/shopify/products/updateRecalculate if type changes
products/delete/webhooks/shopify/products/deleteRemove from database
app/uninstalled/webhooks/shopify/app/uninstalledClean up tenant data

4.5.3 AdPriority to Google Sheets (Scheduled Sync)

The hourly sync job collects all products with pending changes and writes them to the tenant’s Google Sheet via the Sheets API (v4). The job is scheduled via a Bull repeatable job (cron expression) and processed by the worker container.

Bull Repeatable Job (hourly)
    |
    |  1. Query: SELECT * FROM products WHERE needs_sync = true
    |  2. Group by tenant_id
    |
    v
For each tenant:
    |
    |  3. Build row data: [gmc_id, label_0, label_1, label_2, label_3, label_4]
    |  4. Authenticate with tenant's Google OAuth refresh token
    |  5. Call sheets.spreadsheets.values.update()
    |     - Clear existing data
    |     - Write header + all product rows
    |  6. Mark products as synced in database
    |  7. Create sync_log entry
    |
    v
Google Sheet updated
    |
    |  GMC fetches automatically (daily schedule)
    |
    v
Custom labels applied to GMC products within 24 hours

4.5.4 Google Sheets to GMC (Supplemental Feed)

This is a passive integration. Once a Google Sheet is registered as a supplemental feed in GMC, Google fetches it automatically on a daily schedule. AdPriority does not interact with GMC directly in the MVP.

+------------------+        +-------------------+        +------------------+
| Google Sheet     |  PULL  | Google Merchant   |  AUTO  | Google Ads       |
| (AdPriority-     | -----> | Center            | -----> | (PMAX Campaigns) |
|  managed)        | daily  |                   |        |                  |
|                  |        | Matches id column |        | Listing groups   |
| Rows:            |        | to primary feed   |        | filter by        |
|  id              |        | products          |        | custom_label_0   |
|  custom_label_0  |        |                   |        |                  |
|  custom_label_1  |        | Applies labels    |        | Budget allocated |
|  custom_label_2  |        |                   |        | by priority tier |
|  custom_label_3  |        |                   |        |                  |
|  custom_label_4  |        |                   |        |                  |
+------------------+        +-------------------+        +------------------+

4.5.5 Bull Queue Schedule

All scheduled jobs are registered as Bull repeatable jobs and processed by the worker container. Redis persistence ensures jobs survive process restarts.

QUEUE NAME          CRON EXPRESSION    DESCRIPTION
-----------------   -----------------  ----------------------------------------
sync-sheet          0 * * * *          Hourly: Sync pending products to Google Sheet
season-transition   0 0 * * *          Daily:  Check for season transition
new-arrival-expiry  0 1 * * *          Daily:  Expire new arrival boosts
reconciliation      0 3 * * *          Daily:  Reconcile Shopify products
sync-log-cleanup    0 4 * * 0          Weekly: Clean up old sync_logs (> 90 days)
backup-offsite      0 4 * * *          Daily:  pg_dump to Backblaze B2

4.6 Technology Stack Summary

+-------------------+---------------------+------------------------------------+
| Layer             | Technology          | Rationale                          |
+-------------------+---------------------+------------------------------------+
| Frontend          | React 18 + Vite     | Industry standard, fast builds     |
| UI Components     | Shopify Polaris v13  | Native Shopify look and feel       |
| Server State      | React Query v5       | Caching, refetching, pagination    |
| Backend           | Express.js 4.x      | Simple, mature, Shopify ecosystem  |
| Language          | TypeScript 5.x      | Type safety across full stack      |
| ORM               | Prisma 5.x          | Type-safe queries, migrations      |
| Validation        | Zod                 | Schema-first, TypeScript-native    |
| Database          | PostgreSQL 16       | Shared instance, proven, JSONB     |
| Job Queue         | Bull + Redis 7      | Reliable scheduling, retry, visibility |
| Logging           | Pino                | Structured JSON, fast              |
| Deployment        | Docker              | Consistent builds, easy rollback   |
| Ingress           | Cloudflare Tunnel   | Existing infra, TLS, DDoS shield   |
| Version Control   | Git + GitHub        | Standard workflow                  |
+-------------------+---------------------+------------------------------------+

4.7 Scaling Considerations

The MVP architecture is designed for a single-tenant Nexus deployment and early multi-tenant usage (up to approximately 50 tenants). The following table maps growth milestones to architectural changes.

+---------------+--------+----------------------------------------------+
| Tenants       | Phase  | Architecture Change                          |
+---------------+--------+----------------------------------------------+
| 1 (Nexus)     | 0      | Backend + worker + Redis containers           |
+---------------+--------+----------------------------------------------+
| 2-50          | 1-2    | Same architecture, monitor DB query times    |
+---------------+--------+----------------------------------------------+
| 50-200        | 3      | Add connection pooling (PgBouncer)            |
|               |        | Scale worker concurrency                      |
|               |        | PostgreSQL RLS for tenant isolation           |
+---------------+--------+----------------------------------------------+
| 200-1000      | 4+     | Move to managed PostgreSQL (RDS/Supabase)     |
|               |        | Horizontal scaling with load balancer         |
|               |        | Content API replaces Sheets for Pro tier      |
+---------------+--------+----------------------------------------------+
| 1000+         | Future | Multi-region deployment                       |
|               |        | Database read replicas                        |
|               |        | CDN for static assets                         |
+---------------+--------+----------------------------------------------+

The key insight is that complexity is deferred, not designed away. Every scaling change listed above is additive – the core application code does not need rewriting, only the infrastructure around it.


4.8 Chapter Summary

AspectDecision
Architecture styleMonolithic backend + embedded SPA frontend
Deployment targetSingle Docker container on Synology NAS
DatabaseShared PostgreSQL 16 (postgres16 container)
External accessCloudflare Tunnel (no port forwarding)
Auth (merchants)Shopify App Bridge session tokens
Auth (Google)OAuth 2.0 with encrypted refresh tokens
GMC sync (MVP)Google Sheets supplemental feed (daily fetch)
Background jobsBull + Redis job queue (separate worker container)
Scaling pathPgBouncer, RLS, managed DB, multi-region