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.
| Attribute | Value |
|---|---|
| Framework | React 18 |
| Build tool | Vite |
| UI library | Shopify Polaris v13 |
| State mgmt | React Query (TanStack Query v5) |
| Routing | React Router v6 |
| Charts | Chart.js (lightweight) |
| Auth | Shopify App Bridge session tokens |
| Bundle target | < 200 KB gzipped |
Key pages:
| Page | Route | Purpose |
|---|---|---|
| Dashboard | / | Priority distribution, sync status |
| Products | /products | Product list with inline priority edit |
| Rules | /rules | Rule builder and management |
| Calendar | /calendar | Seasonal priority matrix |
| Sync | /sync | Sync status, history, manual trigger |
| Settings | /settings | Google 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.
| Attribute | Value |
|---|---|
| Runtime | Node.js 20 LTS |
| Framework | Express.js 4.x |
| Language | TypeScript 5.x (strict mode) |
| ORM | Prisma 5.x |
| Validation | Zod |
| Logging | Pino (structured JSON) |
| Port | 3010 (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.
| Attribute | Value |
|---|---|
| Container | postgres16 (PostgreSQL 16 Alpine) |
| Database | adpriority_db |
| User | adpriority_user |
| Schema | public |
| Internal port | 5432 (container-to-container) |
| External port | 5433 (NAS host) |
| Network | postgres_default (shared bridge) |
| ORM | Prisma (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).
| Job | Trigger | Frequency |
|---|---|---|
| Shopify product import | Manual / app install | On demand |
| Priority recalculation | Webhook / rule change | Event-driven |
| Google Sheet update | Priority change / cron | Hourly batch |
| Season transition check | Cron | Daily at 00:00 UTC |
| New arrival expiry | Cron | Daily at 01:00 UTC |
| Reconciliation | Cron | Daily at 03:00 UTC |
| Sync log cleanup | Cron | Weekly (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).
| Credential | Storage Location | Encryption |
|---|---|---|
| Shopify access token | tenants.shopify_access_token | AES-256-GCM (v{n}:iv:tag:ciphertext) |
| Google refresh token | tenants.google_refresh_token | AES-256-GCM (v{n}:iv:tag:ciphertext) |
| Shopify API secret | Environment variable | Not in DB |
| Google client secret | Environment variable | Not in DB |
| Database password | Environment variable | Not in DB |
| Encryption master key | Environment variable | Not in DB |
4.4.3 Data Protection
| Concern | Mitigation |
|---|---|
| SQL injection | Prisma parameterized queries (no raw SQL) |
| XSS | React auto-escaping + Polaris components |
| CSRF | Shopify session tokens (stateless, per-request) |
| Rate limiting | Express middleware: 100 req/min API, 10 req/min sync |
| Webhook spoofing | HMAC-SHA256 verification on every webhook |
| Token leakage | Tokens never logged; Pino redaction filters |
| Network exposure | Cloudflare Tunnel (no open ports on NAS) |
| Content injection | CSP headers: frame-ancestors https://*.myshopify.com https://admin.shopify.com; script-src 'self' https://cdn.shopify.com |
| Multi-tenant data leak | All 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:
| Topic | Endpoint | Purpose |
|---|---|---|
products/create | /webhooks/shopify/products/create | Apply initial priority |
products/update | /webhooks/shopify/products/update | Recalculate if type changes |
products/delete | /webhooks/shopify/products/delete | Remove from database |
app/uninstalled | /webhooks/shopify/app/uninstalled | Clean 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
| Aspect | Decision |
|---|---|
| Architecture style | Monolithic backend + embedded SPA frontend |
| Deployment target | Single Docker container on Synology NAS |
| Database | Shared PostgreSQL 16 (postgres16 container) |
| External access | Cloudflare 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 | Bull + Redis job queue (separate worker container) |
| Scaling path | PgBouncer, RLS, managed DB, multi-region |