Chapter 9: Architecture Decision Records
This chapter documents the key architectural decisions made during AdPriority’s design phase. Each decision follows a standardized format: Status, Context, Decision, and Consequences. These records serve as a permanent log of why the system is built the way it is, preventing future re-litigation of settled questions and providing onboarding context for new contributors.
ADR Index
| ADR | Title | Status |
|---|---|---|
| 001 | Google Sheets for MVP sync (not Content API) | Accepted |
| 002 | Express.js over FastAPI | Accepted |
| 003 | Prisma ORM | Accepted |
| 004 | Supplemental feed (not primary feed) | Accepted |
| 005 | 0-5 scoring scale | Accepted |
| 006 | Cloudflare Tunnel for deployment | Accepted |
| 007 | Variant-level IDs in GMC | Accepted |
| 008 | Canonical schema naming (tenants/tenant_id) | Accepted |
| 009 | Two-tier pricing model ($49/$149) | Accepted |
| 010 | Bull+Redis job queue from Phase 1 | Accepted |
| 011 | File-based encryption key versioning | Accepted |
| 012 | PostgreSQL RLS deferred to Phase 2 | Accepted |
| 013 | GraphQL-first for Shopify operations | Accepted |
| 014 | Token Exchange only (no legacy OAuth) | Accepted |
| 015 | Offsite backups to Backblaze B2 | Accepted |
ADR-001: Google Sheets for MVP Sync (Not Content API)
Status: Accepted
Date: 2026-02-10
Context
AdPriority needs to write custom label data to Google Merchant Center so that Google Ads campaigns can use those labels for product segmentation and bid optimization. There are two mechanisms to deliver custom label data to GMC:
-
Google Sheets supplemental feed: AdPriority writes data to a Google Sheet via the Sheets API. GMC is configured to fetch that Sheet on a daily schedule as a supplemental feed. Labels are applied to matching products automatically.
-
Content API for Shopping (v2.1): AdPriority calls the GMC Content API directly to update the
customLabel0throughcustomLabel4fields on each product. Updates take effect within minutes.
The Content API offers faster propagation but introduces significant complexity: OAuth consent for GMC access, per-product rate limits (2 updates/day/product), error handling for 124,060 variants, and merchant-side GMC configuration.
Decision
Use Google Sheets as the sole transport mechanism for the MVP (Phase 0-2).
Consequences
Positive:
- No GMC API authentication required from the merchant (simpler onboarding)
- Sheet is human-readable and debuggable (merchants can inspect the data)
- Proven with live Nexus data: 10/10 test products matched, zero issues
- Google Sheets API is well-documented and has generous quotas
- Merchants can manually edit individual rows in emergencies
- No rate limit concerns (Sheet can contain all 124,060 variants)
Negative:
- Labels propagate within 24 hours (not real-time)
- Merchant must manually register the Sheet as a supplemental feed in GMC (one-time setup, documented in onboarding wizard)
- Full sheet rewrite on each sync (acceptable for < 200k rows)
- Requires Google Sheets API OAuth (lighter than Content API OAuth)
Mitigations:
- Content API will be added in Phase 3+ for Growth/Pro tiers
- 24-hour latency is acceptable because priority changes are strategic (seasonal, weekly) not tactical (minute-by-minute)
- Onboarding wizard provides step-by-step GMC feed setup instructions
Validation
| Test | Result |
|---|---|
| Sample size | 10 active Nexus products |
| Match rate | 10/10 (100%) |
| Attribute recognition | All 5 custom labels recognized |
| Processing time | < 1 hour after manual GMC fetch trigger |
| Issues found | None |
| Test date | 2026-02-10 |
ADR-002: Express.js Over FastAPI
Status: Accepted
Date: 2026-02-06
Context
The backend framework must serve the REST API, handle Shopify OAuth, process webhooks, run scheduled jobs, and serve the compiled React frontend. Two candidates were evaluated:
-
Express.js + TypeScript: The dominant Node.js web framework. Matches Shopify’s official app templates, which use Node.js. The existing reference app at
/volume1/docker/sales-page-app/uses FastAPI (Python), but its Shopify integration required custom adapter code. -
FastAPI (Python): High-performance async framework. Strong typing via Pydantic. The existing
sales-page-appuses this stack, providing a reference implementation.
Decision
Use Express.js with TypeScript for the AdPriority backend.
Consequences
Positive:
- Shopify’s official
@shopify/shopify-app-expresspackage provides production-ready OAuth, session management, and webhook verification - Single language (TypeScript) across frontend and backend reduces context switching
- Prisma ORM has first-class TypeScript support with generated types
- React frontend build outputs static files that Express serves natively
- Bull + Redis provides mature job scheduling with visibility, retry, and concurrency
- NPM ecosystem has packages for Google Sheets API, Google OAuth, and Content API
- Shopify Polaris tooling (CLI, React bindings) assumes a JavaScript ecosystem
Negative:
- Cannot reuse FastAPI code from the existing
sales-page-app - Node.js single-threaded model requires careful handling of CPU-intensive work (not a concern for this I/O-bound application)
- Express.js 4.x requires manual async error handling (mitigated by
express-async-errorspackage)
Alternatives Rejected:
| Alternative | Reason for Rejection |
|---|---|
| FastAPI | Shopify ecosystem friction, separate language from FE |
| NestJS | Unnecessary abstraction for a single-developer project |
| Hono | Newer, smaller ecosystem, less Shopify community |
| Remix/Next.js | Full-stack frameworks add complexity without benefit |
ADR-003: Prisma ORM
Status: Accepted
Date: 2026-02-06
Context
AdPriority requires a database access layer that provides:
- Type-safe queries (prevent runtime SQL errors)
- Migration management (versioned schema changes)
- Multi-tenant middleware support (automatic
tenant_idscoping) - PostgreSQL compatibility
- Reasonable learning curve for a single developer
Three ORMs were evaluated: Prisma, Drizzle, and Knex.
Decision
Use Prisma 5.x as the ORM for AdPriority.
Consequences
Positive:
- Type safety: Generated TypeScript types from schema definition. Query results are fully typed; field name typos are compile-time errors.
- Schema-as-code:
schema.prismaserves as the single source of truth for the database schema. Human-readable, version-controlled. - Migrations:
prisma migrate devgenerates SQL migration files automatically from schema changes. Migration history is tracked. - Multi-tenant middleware: Prisma Client Extensions support the
getTenantClient()pattern described in Chapter 6, enabling automatictenant_idscoping on all queries. - Tooling:
prisma studioprovides a GUI database browser for debugging.prisma generateregenerates the client after schema changes. - Ecosystem: Widely adopted, extensive documentation, active community.
Negative:
- Prisma’s query engine adds a binary dependency (~20 MB) to the Docker image.
- Complex raw SQL queries (e.g., window functions, CTEs) require
prisma.$queryRaw, losing some type safety. - N+1 query patterns require explicit
includestatements (not automatically optimized). - Schema changes require running
prisma migrate dev, which may conflict withprisma db pushif used inconsistently (project will use migrations exclusively).
Alternatives Rejected:
| Alternative | Reason for Rejection |
|---|---|
| Drizzle | Better SQL-level control but weaker migration tooling; less mature middleware extension API |
| Knex | Query builder only (no ORM layer), manual type definitions, no middleware pattern |
| TypeORM | Decorator-heavy, historically buggy, declining community |
| Raw SQL | No type safety, manual migration management, high maintenance burden |
Schema Example
model Product {
id String @id @default(uuid())
tenantId String @map("tenant_id")
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
shopifyProductId BigInt @map("shopify_product_id")
shopifyVariantId BigInt? @map("shopify_variant_id")
title String?
productType String? @map("product_type")
priority Int @default(3)
prioritySource String @default("default") @map("priority_source")
priorityLocked Boolean @default(false) @map("priority_locked")
syncStatus String @default("pending") @map("sync_status")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@unique([tenantId, shopifyProductId, shopifyVariantId])
@@index([tenantId])
@@index([tenantId, priority])
@@map("products")
}
ADR-004: Supplemental Feed (Not Primary Feed Replacement)
Status: Accepted
Date: 2026-02-06
Context
Google Merchant Center supports two feed types:
-
Primary feed: The authoritative source of product data (title, price, availability, images, etc.). For Shopify stores, this is typically managed by the Google & YouTube Shopify channel app, which syncs product data automatically.
-
Supplemental feed: An additive feed that overlays additional attributes onto products already present in the primary feed. It can add or overwrite specific fields (like custom labels) without affecting the core product data.
AdPriority needs to set custom_label_0 through custom_label_4 on GMC
products. This can be done by either replacing the primary feed (taking over
all product data management) or by adding a supplemental feed (only touching
the custom label fields).
Decision
Use a supplemental feed exclusively. Never replace or interfere with the primary feed.
Consequences
Positive:
- Non-destructive: AdPriority cannot accidentally break product titles, prices, images, or availability in GMC. The primary feed remains untouched.
- Additive only: Custom labels are layered on top of existing product data. If AdPriority is uninstalled, labels simply revert to empty on the next primary feed update.
- Coexistence: Merchants can use AdPriority alongside other feed management tools without conflict.
- Simpler scope: AdPriority only needs to manage 6 columns (id + 5 labels), not the full product catalog schema (30+ fields).
- Lower risk: A bug in AdPriority cannot cause products to disappear from GMC or display incorrect prices.
Negative:
- Products must already exist in the primary feed before supplemental labels can be applied. If a product is not in GMC (e.g., not synced by the Shopify Google channel), AdPriority cannot add it.
- The
idcolumn in the supplemental feed must exactly match the primary feed’s product IDs. Any mismatch (case-sensitive) results in the label being silently ignored.
Mitigations:
- Reconciliation job (daily) checks for products in the AdPriority database that have no match in GMC, alerting the merchant.
- Onboarding wizard verifies that the Shopify Google channel is active before proceeding with feed setup.
ADR-005: 0-5 Scoring Scale
Status: Accepted
Date: 2026-02-06
Context
AdPriority needs a priority scoring system that:
- Is simple enough for non-technical merchants to understand immediately
- Maps cleanly to Google Ads campaign segmentation (listing group filters)
- Provides enough granularity for meaningful budget differentiation
- Works with GMC custom label constraints (max 1,000 unique values per label)
Scales considered:
| Scale | Granularity | Simplicity | Label Values |
|---|---|---|---|
| 0-1 | Low | High | 2 |
| 0-5 | Medium | High | 6 |
| 0-10 | High | Medium | 11 |
| 0-100 | Very high | Low | 101 |
| A/B/C/D/F | Medium | High | 5 |
Decision
Use an integer scale from 0 to 5, where 0 means “exclude from advertising” and 5 means “push hard with maximum budget.”
Consequences
Score definitions:
+-------+-------------+------------------------------------------+
| Score | Label | Google Ads Behavior |
+-------+-------------+------------------------------------------+
| 5 | Push Hard | Maximum budget, aggressive bidding |
| 4 | Strong | High budget, balanced approach |
| 3 | Moderate | Standard budget, conservative bidding |
| 2 | Light | Minimal budget, strict ROAS targets |
| 1 | Minimal | Very low budget, highest ROAS only |
| 0 | Exclude | No advertising spend |
+-------+-------------+------------------------------------------+
Positive:
- Six values (0-5) map perfectly to 6 PMAX asset groups or listing group subdivisions, each with its own budget allocation.
- Score 0 has a clear, unambiguous meaning: do not advertise this product. This handles out-of-stock, discontinued, and deliberately excluded items.
- The scale is intuitive. Merchants think in terms of “high priority” vs. “low priority,” not percentages or letter grades.
- Only 6 unique values in
custom_label_0, well within GMC’s 1,000 limit. - Integer type in the database with a CHECK constraint (0-5) prevents invalid values.
Negative:
- Six tiers may feel coarse for large catalogs (10,000+ products). Merchants may want finer control within a tier.
- No decimal values (e.g., 3.5 for “between moderate and strong”).
Mitigations:
- The other four custom labels (
custom_label_1throughcustom_label_4) provide additional segmentation dimensions (season, category, status, brand tier). Combined with the priority score, this gives merchants 6 x 4 x ~15 x 5 x 3 = ~5,400 possible product segments. - Future enhancement: sub-tiers within a score (e.g., 4a, 4b) if merchant feedback indicates a need.
ADR-006: Cloudflare Tunnel for Deployment
Status: Accepted
Date: 2026-02-06
Context
Shopify apps must be accessible over HTTPS from Shopify’s servers for:
- OAuth callback URLs
- Webhook delivery
- App Bridge iFrame loading
The AdPriority backend runs on a Synology NAS on a local network (192.168.1.26). Making it accessible from the internet requires one of:
| Option | Complexity | Cost | Security |
|---|---|---|---|
| Port forwarding + DDNS | Low | Free | Exposes port |
| Reverse proxy (Nginx) | Medium | Free | Requires static IP |
| Cloudflare Tunnel | Low | Free | No open ports |
| Cloud VM (AWS/GCP) | High | $20+/mo | Full control |
| Ngrok | Low | $10+/mo | Ephemeral URLs |
Decision
Use Cloudflare Tunnel (via the existing cloudflared service at
/volume1/docker/services/cloudflared/) to expose the AdPriority backend
over HTTPS.
Consequences
Positive:
- Existing infrastructure: The
cloudflaredcontainer is already running on the NAS for other services. Adding a route for AdPriority requires only a configuration change, not a new deployment. - No open ports: The NAS firewall does not need any inbound port rules. The tunnel agent initiates an outbound connection to Cloudflare’s edge, which then proxies inbound traffic through the tunnel.
- Free TLS: Cloudflare provides TLS certificates automatically. No need to manage Let’s Encrypt or self-signed certificates.
- DDoS protection: Cloudflare’s edge network provides basic DDoS mitigation, rate limiting, and bot detection.
- Stable URLs: The tunnel provides a permanent hostname
(e.g.,
app.adpriority.com) that does not change on NAS reboot or IP change. - Zero cost: Cloudflare Tunnels are free for the traffic volumes AdPriority will generate.
Negative:
- Dependency on Cloudflare’s infrastructure. If Cloudflare has an outage, the app is unreachable (rare, but possible).
- Added latency: traffic routes through Cloudflare’s nearest edge node before reaching the NAS. Adds ~10-30ms per request (negligible for this use case).
- Tunnel token must be stored securely (already managed via
.envfile).
Configuration:
# cloudflared tunnel configuration (addition)
ingress:
- hostname: app.adpriority.com
service: http://adpriority:3010
# ... existing routes ...
- service: http_status:404
ADR-007: Variant-Level IDs in GMC
Status: Accepted
Date: 2026-02-10
Context
Google Merchant Center identifies Shopify products using a composite ID format. Analysis of the Nexus GMC export (124,060 products, exported 2026-02-10) revealed that all products use variant-level IDs, not product-level IDs.
Observed format:
shopify_US_{productId}_{variantId}
Real examples from the Nexus GMC export:
shopify_US_8779355160808_46050142748904
shopify_US_9128994570472_47260097118440
shopify_US_9057367064808_47004004712680
shopify_US_9238797418728_47750439567592
shopify_US_7609551716584_42582395650280
No product-level IDs (without the variant suffix) were found in the export. This means that even if a Shopify product has only one variant, GMC still represents it with the variant-level ID.
Decision
AdPriority will generate and match GMC product IDs at the variant level
exclusively, using the format shopify_US_{productId}_{variantId}.
Consequences
Positive:
- 100% match rate with Nexus GMC products (validated with 10/10 test products)
- Future-proof: supports products with multiple variants (size x color), where each variant can have a different inventory status
- Enables variant-level priority overrides in future versions (e.g., exclude out-of-stock sizes while keeping in-stock sizes at priority 5)
Negative:
- More rows in the Google Sheet: Nexus has ~5,582 products but ~20,000 active variants, and 124,060 total GMC entries. The supplemental feed must include one row per variant, not one row per product.
- Priority is set at the product level in the MVP, meaning all variants of a product share the same priority score. This results in duplicate label values across variants (acceptable, not wasteful).
- Slightly more complex ID generation: must fetch variant IDs from Shopify, not just product IDs.
Key data points:
+--------------------------------+--------+
| Metric | Value |
+--------------------------------+--------+
| Nexus products in Shopify | 5,582 |
| Nexus active products | 2,425 |
| Nexus variants in GMC | 124,060|
| Estimated active variants | ~20,000|
| ID format confirmed | Yes |
| Product-only IDs found | 0 |
| Country code (Nexus) | US |
| Product ID length | 13 digits |
| Variant ID length | 14 digits |
+--------------------------------+--------+
GMC ID construction:
function buildGmcProductId(
countryCode: string, // "US"
productId: bigint, // 8779355160808
variantId: bigint // 46050142748904
): string {
return `shopify_${countryCode}_${productId}_${variantId}`;
// Result: "shopify_US_8779355160808_46050142748904"
}
Item Group ID (used for variant grouping in GMC): Just the Shopify product ID without prefix or variant suffix.
Item Group ID: 8779355160808
This groups all variants of a product together in GMC reporting and campaign
structure while allowing individual variant targeting via the full
shopify_US_{productId}_{variantId} offer ID.
ADR-008: Canonical Schema Naming (tenants/tenant_id)
Status: Accepted
Date: 2026-02-15
Context
The blueprint inconsistently uses stores/store_id (Ch.8, root docs) and
tenants/tenant_id (Ch.14). Three of six expert reviewers flagged this as a
cross-cutting concern that affects every query, every middleware function, and
every API contract in the system.
Decision
Standardize on tenants/tenant_id/tenantId from Ch.14’s mature schema
definition. All references to stores/store_id/storeId are renamed
throughout the codebase and documentation.
Consequences
Positive:
- SaaS-standard terminology aligns with industry conventions (Stripe, Auth0, Clerk all use “tenant”)
- Eliminates confusion between Shopify “stores” (the merchant’s shop) and AdPriority “tenants” (the billing/data-isolation entity)
- Single consistent naming across schema, API, middleware, and documentation
Negative:
- 130+ renames across documentation
- Developers familiar with the old naming must adjust
Mitigations:
- All renames applied in a single coordinated update (Blueprint v2.0.0)
@@map("tenants")in Prisma schema ensures the database table name matches
ADR-009: Two-Tier Pricing Model ($49/$149)
Status: Accepted
Date: 2026-02-15
Context
The original 4-tier pricing model (Starter $29 / Growth $79 / Pro $199 / Enterprise Custom) had a weak Starter value proposition. A retail business reviewer noted that the $29 tier provides no meaningful differentiator over free alternatives and increases churn risk. The Growth and Pro tiers had overlapping feature sets that confused prospects.
Decision
Simplify to two tiers:
- AdPriority ($49/mo): All core features (priority scoring, Google Sheets sync, seasonal calendar, rules engine, Shopify webhooks)
- AdPriority Pro ($149/mo): Adds Google Ads integration, ROAS tracking, Content API direct sync, and priority support
Consequences
Positive:
- Higher minimum ARPU ($49 vs $29)
- Simpler billing implementation (two Shopify recurring charges vs four)
- Clearer feature differentiation between tiers
- Reduced support burden (fewer tier-related questions)
- Easier to communicate value proposition in App Store listing
Negative:
- Higher entry price may deter some small merchants
- No free or trial-priced tier (mitigated by Shopify’s standard 7-day free trial)
Mitigations:
- 7-day free trial via Shopify Billing API for all new installs
- $49 base tier includes all features needed for immediate ROI
- Upgrade path to Pro is self-service with no data migration required
ADR-010: Bull+Redis Job Queue from Phase 1
Status: Accepted
Date: 2026-02-15
Context
Ch.6 originally used inline node-cron for scheduling, while Ch.24 specified
Bull+Redis queue. An architecture reviewer flagged the unclear transition point
and the risk of lost jobs when the Express process restarts during a cron cycle.
Decision
Use Bull+Redis from Phase 1 onwards. No node-cron usage anywhere in the
application. All scheduled and event-driven work flows through Bull queues.
Consequences
Positive:
- Job visibility via Bull Board UI (queue depth, failed jobs, retry history)
- Automatic retry with exponential backoff on transient failures
- Concurrency control (process N jobs simultaneously, configurable per queue)
- Dead letter queues for jobs that exceed max retry attempts
- Separate worker process isolates background work from API request handling
- No lost jobs on process restart (Redis persistence)
Negative:
- Adds Redis as an infrastructure dependency from day one
- Slightly more complex local development setup (need Redis container)
- Additional memory footprint (~50 MB for Redis with typical workload)
Mitigations:
- Redis 7 Alpine image is lightweight and well-supported on Synology NAS
docker-compose.ymlincludes Redis with health checks from initial setup- Bull Board dashboard provides operational visibility without custom tooling
ADR-011: File-Based Encryption Key Versioning
Status: Accepted
Date: 2026-02-15
Context
A security reviewer flagged single ENCRYPTION_KEY with no rotation capability
as a Critical finding (SEC-02). If the key is compromised, all encrypted tokens
(Shopify access tokens, Google refresh tokens) must be re-encrypted
simultaneously, requiring application downtime.
Decision
Use versioned ciphertext format: v{n}:iv:tag:ciphertext. New encryptions
always use the latest key version. Decryption reads the version prefix to
select the correct key from environment variables (ENCRYPTION_KEY_V1,
ENCRYPTION_KEY_V2, etc.).
Consequences
Positive:
- Zero-downtime key rotation: add new key version, new encryptions use it, old ciphertexts remain decryptable
- No mass re-encryption needed during rotation
- Backward-compatible decryption (version prefix selects correct key)
- Audit trail: ciphertext version indicates when a token was last encrypted
Negative:
- Slightly more complex encrypt/decrypt functions
- Environment variable count grows with each rotation (one per version)
- Must never delete old key versions while any ciphertext references them
Mitigations:
- Lazy re-encryption: when a token is decrypted with an old key, re-encrypt with the latest key before storing. Over time, all tokens migrate to the current version.
- Key version cleanup script identifies orphaned versions with zero references
ADR-012: PostgreSQL RLS Deferred to Phase 2
Status: Accepted
Date: 2026-02-15
Context
Security and architecture reviewers flagged application-only tenant isolation
as a risk. If a bug in the Prisma middleware fails to apply the tenant_id
filter, one tenant could access another’s data. Row-Level Security (RLS)
provides database-level defense-in-depth by enforcing tenant isolation at the
PostgreSQL level.
Decision
Accept Prisma middleware tenant scoping for Phase 0-1 (fewer than 50 tenants). Implement PostgreSQL RLS policies in Phase 2 when tenant count justifies the added complexity.
Consequences
Positive:
- Lower Phase 1 complexity (no RLS policy management)
- Prisma middleware is simpler to test and debug
- Clear migration path documented for Phase 2
Negative:
- Accepted risk: a middleware bug could leak cross-tenant data in Phase 0-1
- No database-level enforcement until Phase 2
Mitigations:
- Comprehensive integration tests verify tenant isolation on every query
- Code review checklist includes “Does this query scope by
tenantId?” - RLS policies pre-designed and documented for Phase 2 implementation
- Tenant count monitored; if growth exceeds 50 tenants before Phase 2, RLS implementation is accelerated
ADR-013: GraphQL-First for Shopify Operations
Status: Accepted
Date: 2026-02-15
Context
A Shopify platform reviewer flagged REST vs GraphQL inconsistency as a Critical finding. Ch.6 specified GraphQL for product fetching, but Ch.10 used REST API exclusively for integration examples. Shopify is actively deprecating REST API features in favor of the GraphQL Admin API.
Decision
Use the Shopify GraphQL Admin API for all operations: products, metafields, webhooks, billing, and shop data. REST API is permitted only where the GraphQL Admin API has documented gaps.
Consequences
Positive:
- Future-proof: aligns with Shopify’s platform direction
- More efficient queries (request only needed fields, reduce payload size)
- Bulk operations via
bulkOperationRunQueryfor large catalogs - Better type safety with GraphQL code generation
- Single API style across the entire codebase
Negative:
- Cursor-based pagination replaces
since_id(different mental model) - Cost-based rate limiting replaces call-based (requires cost calculation)
- Requires learning GraphQL query patterns if unfamiliar
- Some operations have different field names between REST and GraphQL
Mitigations:
- Shopify’s GraphQL Explorer provides interactive query building
- Rate limit cost is logged per query for monitoring
- Pagination helper utility wraps cursor-based iteration
ADR-014: Token Exchange Only (No Legacy OAuth)
Status: Accepted
Date: 2026-02-15
Context
A Shopify platform reviewer flagged contradictory auth flows. The blueprint
documented both the legacy authorization code exchange flow (with /auth/callback
endpoint) and App Bridge Token Exchange. These are mutually exclusive approaches
for Shopify embedded apps.
Decision
Remove all legacy authorization code flow documentation and code. Use Shopify App Bridge v4.1 Token Exchange exclusively for authentication and session management.
Consequences
Positive:
- No
/auth/callbackendpoint needed (simpler routing) - No redirect-based OAuth dance (better merchant UX)
- Seamless session management via App Bridge
- Aligns with Shopify’s recommended modern approach
- Reduced attack surface (no authorization code interception risk)
Negative:
- Requires App Bridge 4.1+ (minimum Shopify API version constraint)
- Cannot support non-embedded usage (acceptable per Shopify’s embedded mandate)
Mitigations:
- App Bridge 4.1 is stable and well-documented
- Shopify’s
@shopify/shopify-app-expresspackage supports Token Exchange natively - All new Shopify apps are required to be embedded, making Token Exchange the standard path
ADR-015: Offsite Backups to Backblaze B2
Status: Accepted
Date: 2026-02-15
Context
A DevOps reviewer flagged the absence of offsite backups as a Critical finding. All database backups were stored on the same Synology NAS that hosts the database. A hardware failure (disk array, power supply, motherboard) would result in total data loss – both production data and all backups.
Decision
Daily automated pg_dump to Backblaze B2 via rclone. 30-day retention
policy. Weekly backup verification by restoring to a temporary database and
validating row counts against production.
Consequences
Positive:
- Geographic separation: backups survive NAS hardware failure
- S3-compatible API enables future cloud migration (backups are portable)
- Low cost (~$5/TB/month for B2 storage)
rcloneruns in a lightweight Docker container on the NAS- Weekly verification catches silent corruption before it matters
Negative:
- Requires Backblaze B2 account and
rcloneconfiguration - Recovery time includes download from B2 + restore (estimated 1-4 hours depending on database size)
- Upload bandwidth consumed daily (mitigated by small database size)
Mitigations:
- Documented recovery runbook with step-by-step restore procedure
rclonesupports bandwidth limiting to avoid saturating the NAS uplink- Backup job runs at 04:00 UTC (off-peak hours)
- Alert if backup job fails or verification detects row count mismatch
ADR Template
Future architecture decisions should follow this template:
## ADR-NNN: [Title]
**Status**: Proposed | Accepted | Deprecated | Superseded by ADR-NNN
**Date**: YYYY-MM-DD
### Context
[Describe the situation, constraints, and forces at play.
What problem are we solving? What options exist?]
### Decision
[State the decision clearly in one or two sentences.]
### Consequences
**Positive**:
- [Benefit 1]
- [Benefit 2]
**Negative**:
- [Trade-off 1]
- [Trade-off 2]
**Mitigations**:
- [How negative consequences are addressed]
Chapter Summary
These fifteen ADRs establish the architectural foundation for AdPriority:
| ADR | Core Principle |
|---|---|
| 001 | Simple transport first; optimize later |
| 002 | Match the ecosystem; do not fight the platform |
| 003 | Type safety and developer experience over raw performance |
| 004 | Be additive, not destructive; minimize blast radius |
| 005 | Optimize for merchant comprehension, not engineer precision |
| 006 | Reuse existing infrastructure; minimize operational burden |
| 007 | Match the platform’s data model exactly; do not abstract it |
| 008 | One name for one concept; consistency eliminates ambiguity |
| 009 | Fewer tiers, higher floor; simplicity converts better |
| 010 | Reliable infrastructure from day one; avoid rework |
| 011 | Rotate secrets without downtime; plan for compromise |
| 012 | Defer complexity until scale justifies it |
| 013 | Follow the platform’s direction; do not fight deprecation |
| 014 | One auth flow; eliminate contradictions |
| 015 | Survive hardware failure; backups must be offsite |
The common thread across all decisions is pragmatism over purity. Every choice prioritizes shipping a working MVP on existing infrastructure over theoretical architectural elegance. Complexity is deferred to later phases where it can be justified by real usage data and paying customers.