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.
| 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):
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.
| 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) |
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.
| Credential | Storage Location | Encryption |
|---|---|---|
| Shopify access token | stores.shopify_access_token | AES-256-GCM |
| Google refresh token | stores.google_refresh_token | AES-256-GCM |
| 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) |
| Multi-tenant data leak | All 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:
| 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).
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
| 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 (MVP) | Inline node-cron (no Redis/Bull) |
| Scaling path | Extract worker, add Redis, managed DB, multi-region |