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  | |Scheduler |   |
 |                      |    16    | | (inline) | |  (cron)  |   |
 |                      |adpriority| |          | |          |   |
 |                      |   _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: season  |
                              | 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):

stores              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 (Inline Background Jobs)

For the MVP, background jobs run inline within the Express process using node-cron for scheduling and a simple in-memory queue for webhook-triggered work. This eliminates the need for Redis and Bull in Phase 0-2, reducing infrastructure complexity.

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)

Future (Phase 3+): When scaling to multi-tenant SaaS, the worker will be extracted into a separate process backed by Bull + Redis for reliable job queuing, retry semantics, and concurrency control.

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 (Starter 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:AdPrioritySecure2026@postgres16:5432/adpriority_db
      SHOPIFY_API_KEY: ${SHOPIFY_API_KEY}
      SHOPIFY_API_SECRET: ${SHOPIFY_API_SECRET}
      GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
      GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
    networks:
      - postgres_default

networks:
  postgres_default:
    external: true

Production (docker-compose.prod.yml):

services:
  adpriority:
    image: adpriority:${VERSION:-latest}
    restart: always
    environment:
      NODE_ENV: production
      DATABASE_URL: ${DATABASE_URL}
      ENCRYPTION_KEY: ${ENCRYPTION_KEY}
    networks:
      - postgres_default
      - adpriority_internal
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:3010/health"]
      interval: 30s
      timeout: 5s
      retries: 3

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

4.3.3 Single-Container Strategy

For the MVP, the backend serves the compiled frontend as static files from a single Express process. This simplifies deployment, networking, and resource usage:

adpriority container (Node.js 20)
|
+-- Express server (:3010)
|   +-- /api/v1/*          --> API routes
|   +-- /auth/*            --> OAuth routes
|   +-- /webhooks/*        --> Shopify webhooks
|   +-- /health            --> Liveness probe
|   +-- /*                 --> Static files (React build)
|
+-- Inline worker
|   +-- node-cron jobs
|   +-- Webhook event queue
|
+-- Prisma Client
    +-- postgres16:5432/adpriority_db

This mirrors the architecture of the existing sales-page-app reference implementation at /volume1/docker/sales-page-app/, which also serves a React + Polaris frontend from a single backend process.


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 an environment variable (ENCRYPTION_KEY), never in the database or code repository.

CredentialStorage LocationEncryption
Shopify access tokenstores.shopify_access_tokenAES-256-GCM
Google refresh tokenstores.google_refresh_tokenAES-256-GCM
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)
Multi-tenant data leakAll queries scoped by store_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 store_id from token
    |  3. Query database (scoped by store_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. Queue for processing (inline worker)
    |  4. Return 200 OK immediately
    |
    v
Inline Worker
    |
    |  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).

Scheduler (node-cron, hourly)
    |
    |  1. Query: SELECT * FROM products WHERE needs_sync = true
    |  2. Group by store_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 Cron Job Schedule

All scheduled jobs run within the backend process via node-cron.

MINUTE  HOUR  DAY  DESCRIPTION
------  ----  ---  ----------------------------------------
  0      *     *   Hourly: Sync pending products to Google Sheet
  0      0     *   Daily:  Check for season transition
  0      1     *   Daily:  Expire new arrival boosts
  0      3     *   Daily:  Reconcile Shopify products
  0      4     0   Weekly: Clean up old sync_logs (> 90 days)

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     |
| Scheduling        | node-cron           | Lightweight, no Redis dependency   |
| 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      | Single container, inline worker, node-cron   |
+---------------+--------+----------------------------------------------+
| 2-50          | 1-2    | Same architecture, monitor DB query times    |
+---------------+--------+----------------------------------------------+
| 50-200        | 3      | Extract worker to separate container          |
|               |        | Add Redis for job queue (Bull)                |
|               |        | Add connection pooling (PgBouncer)            |
+---------------+--------+----------------------------------------------+
| 200-1000      | 4+     | Move to managed PostgreSQL (RDS/Supabase)     |
|               |        | Horizontal scaling with load balancer         |
|               |        | Content API replaces Sheets for Pro/Ent tiers |
+---------------+--------+----------------------------------------------+
| 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 jobs (MVP)Inline node-cron (no Redis/Bull)
Scaling pathExtract worker, add Redis, managed DB, multi-region