1

Authentication Per Surface

Each surface uses the authentication method appropriate to its execution context. Cart uses App Proxy HMAC, UI Extensions use Session Token JWT, and the Admin dashboard relies on standard Shopify OAuth.

Surface Auth Method Details
Cart (App Proxy) HMAC Shopify signs request with app secret, timing-safe comparison
Checkout JWT sessionToken.get() in extension
Thank You JWT sessionToken.get() in extension
Customer Account JWT sessionToken.get() in extension
Admin OAuth Standard Shopify app auth flow
External API Bearer API key from Pro plan (post-MVP)
2

App Proxy HMAC Verification

Shopify signs every App Proxy request with the application's API secret. The backend verifies authenticity through HMAC-SHA256 with timing-safe comparison to prevent signature forgery.

🛡
Request Signing
Shopify signs each proxy request with the app's API secret key before forwarding to the backend.
  • All query parameters collected from request
  • Parameters sorted alphabetically by key
  • HMAC-SHA256 digest computed against sorted params
  • Hex-encoded digest appended to forwarded request
🔒
Timing-Safe Comparison
Verification uses crypto.timingSafeEqual to prevent timing attacks that could leak valid signatures byte by byte.
  • Constant-time buffer comparison
  • Prevents statistical timing analysis
  • Returns 401 on mismatch, no error details
  • IP extracted from X-Forwarded-For header
hmac-verification.ts
// Sorted params → HMAC-SHA256 → hex comparison
const sortedParams = Object.keys(query)
  .filter(key => key !== 'signature')
  .sort()
  .map(key => `${key}=${query[key]}`)
  .join('');

const digest = crypto
  .createHmac('sha256', API_SECRET)
  .update(sortedParams)
  .digest('hex');

// Timing-safe: prevents timing attacks
const isValid = crypto.timingSafeEqual(
  Buffer.from(digest),
  Buffer.from(signature)
);
3

Session Token Handling

UI Extensions authenticate using JWT tokens obtained from Shopify's SDK. A wrapper pattern handles automatic token refresh with double retry for seamless error recovery.

session-token-flow.diagram
                    SESSION TOKEN LIFECYCLE

  +-------------------+         +-------------------+
  | UI Extension      |         | Shopify SDK       |
  |  Checkout          |         |                   |
  |  Thank You         | ──1──> | sessionToken     |
  |  Customer Account  | <──2── |  .get()           |
  +--------+----------+         +-------------------+
           |
           | 3  Authorization: Bearer <JWT>
           v
  +-------------------+         +-------------------+
  | Backend API       |         | 401 Response?     |
  |  Verify JWT        | ──4──> |                   |
  |  Extract shop      |         |  Yes → Get new token|
  |  Process request   |         |        Retry request|
  +-------------------+         |        Still 401?   |
                                  |         Show error  |
                                  +-------------------+
🔑
Token Acquisition
  • Extensions call sessionToken.get() from Shopify SDK
  • Returns a signed JWT containing shop domain and session data
  • Token sent as Authorization: Bearer header
  • Backend verifies JWT signature and extracts shop context
Double Retry Pattern
  • fetchWithTokenRefresh wrapper used in all extensions
  • 401 response triggers automatic token refresh
  • Request retried with new token
  • If still 401 on second attempt, show error to user
4

Shopify Required Scopes

Minimum required OAuth scopes for reading and writing order and customer data. Each scope is justified by a specific functional requirement.

read_orders READ
Read order data for metafield operations. Required to check existing VAT data on orders.
write_orders WRITE
Write order metafields with VAT validation data including VAT number, company name, and reverse charge status.
read_customers READ
Read customer metafields for auto-populating VAT numbers. Required for customer lookup by email during guest checkout.
write_customers WRITE
Write customer metafields and set taxExempt property for automatic VAT exemption on validated B2B customers.
⚠ Deprecated Scopes
read_checkouts and write_checkouts are deprecated by Shopify. Checkout UI Extensions use session token-based authentication and do not require these scopes.
5

Webhook Security

Five mandatory webhooks handle lifecycle events, GDPR compliance, and order synchronization. All webhooks are verified via Shopify's HMAC-SHA256 signature and processed with idempotency guarantees.

Lifecycle
APP_UNINSTALLED
Cleanup shop data on app removal. Deletes shop record with CASCADE to remove all associated validations, settings, and logs.
GDPR
SHOP_REDACT
GDPR shop deletion request, triggered 48 hours after uninstall. Idempotent handler ensures complete data removal even if APP_UNINSTALLED already processed.
GDPR
CUSTOMERS_REDACT
GDPR PII anonymization. Nullifies personal data (email, IP) while preserving business data (VAT numbers, company names) for 7-year tax compliance retention.
GDPR
CUSTOMERS_DATA_REQUEST
GDPR data export request. Queries customer VAT profiles and validation history, returning data to Shopify for merchant delivery to the customer. App Store review mandatory.
Data Sync
ORDERS_CREATE
Syncs cart attributes to order metafields when checkout completes. Triggers reverse charge evaluation and tax exemption processing for the completed order.
🔄 Webhook Idempotency
1
Shopify guarantees "at least once" delivery. The same webhook may be delivered multiple times during retries or infrastructure issues.
2
The webhook_logs table enforces a unique index on (order_id, topic) to detect duplicate deliveries at the database level.
3
On duplicate insert, PostgreSQL raises error code 23505 (unique_violation). The handler catches this and skips processing.
4
This prevents duplicate metafield writes, duplicate counter increments, and duplicate tax exemption mutations.
6

GDPR Compliance

Comprehensive GDPR compliance with clear data classification, purpose-specific retention periods, and automated anonymization. VAT business data is preserved for tax compliance while personal data is immediately redacted.

Data Classification

Data Type Classification GDPR Scope
VAT numbers Business data Outside GDPR
Company names Business data Outside GDPR
Customer email Personal data (PII) GDPR Applies
Customer ID Personal data (PII) GDPR Applies
IP address Personal data (PII) GDPR Applies

CUSTOMERS_REDACT Handling

Data After Redaction Retention
customer_email NULL (deleted) Immediate
ip_address NULL (deleted) Immediate
customer_id REDACTED_{timestamp} Immediate
vat_number PRESERVED 7 years (tax compliance)
company_name PRESERVED 7 years (tax compliance)
is_valid, validated_at PRESERVED 7 years (tax compliance)
Legal Basis
Article 6(1)(b)
Processing necessary for the performance of a contract to which the data subject is party.
Data Retention
7 Years
Tax compliance data retained per EU Directive requirements for VAT transaction records.
Data Region
EU Frankfurt
Supabase hosted in EU Frankfurt region to meet GDPR data residency requirements.
VAT Data Exemption
Recital 14
VAT numbers and company names are legal entity data, outside GDPR scope per Recital 14.
ⓘ CUSTOMERS_DATA_REQUEST
Queries customer_vat_profiles and vat_validations tables. Returns structured data to Shopify, which the merchant delivers to the requesting customer. This webhook handler is mandatory for Shopify App Store review.
7

Tax Exemption

Automated VAT exemption based on customer login status and plan tier. Logged-in customers on all plans get automatic exemption; guest exemption requires Shopify Plus for customer creation capabilities.

Logged-in Customer All Plans

1
VAT number validated as valid by VIES
2
Check: autoExemptionEnabled in shop settings?
3
Check: same country as seller? If exemptionSameCountry=false, skip exemption
4
Admin API: customer.taxExempt = true
5
Add tag: vat-exempt to customer
6
Shopify automatically removes tax from cart/checkout

Guest Customer Plus Only

1
VAT validated in Checkout UI Extension
2
Get guest email from checkout context
3
Search existing customer by email (Admin API)
4
If not found: create new customer with tag vat-auto-created
5
Set taxExempt = true on customer
6
Shopify recalculates tax for the order
Guest (Non-Plus)
VAT is validated but no automatic exemption is applied. The extension displays: "Log in for VAT tax exemption" to encourage account creation for future purchases.

Edge Cases

Empty / invalid email
Skip exemption flow entirely. VAT validation still proceeds and result is stored, but no customer mutation occurs.
Existing customer found by email
customerUpdate mutation sets taxExempt: true and adds the vat-exempt tag to the existing customer record.
New customer needed
customerCreate mutation creates the customer with taxExempt: true and tags vat-exempt, vat-auto-created.
Merchant disabled auto-exemption
VAT is validated and stored normally. No customer mutation occurs. Merchant handles exemption manually via admin.
Customer create fails
VAT validation result: valid. Exemption result: false. Error is logged for admin visibility. Customer sees valid VAT confirmation.
8

Reverse Charge

EU intra-community B2B reverse charge mechanism per Article 196, Council Directive 2006/112/EC. When seller and buyer are in different EU countries with a valid VAT number, the buyer self-assesses VAT.

reverse-charge-evaluation.diagram
                  REVERSE CHARGE EVALUATION LOGIC

  +-------------------+
  | Start Evaluation  |
  +--------+----------+
           |
           v
  +-------------------+     NO     +-------------------+
  | RC enabled?       | ────────> | Skip (disabled)   |
  +--------+----------+            +-------------------+
           | YES
           v
  +-------------------+     NULL   +-------------------+
  | Seller country?   | ────────> | Skip + warning    |
  +--------+----------+            +-------------------+
           | SET
           v
  +-------------------+     NO     +-------------------+
  | VAT valid?        | ────────> | Not applicable    |
  +--------+----------+            +-------------------+
           | YES
           v
  +-------------------+     YES    +-------------------+
  | Same country?     | ────────> | Not applicable    |
  +--------+----------+            +-------------------+
           | NO
           v
  +-------------------+     NO     +-------------------+
  | Both EU members?  | ────────> | Not applicable    |
  +--------+----------+            +-------------------+
           | YES
           v
  +-------------------+
  | RC APPLICABLE     |     Write to order metafields:
  |  reverse_charge    |       reverse_charge = true
  |  = true            |       reverse_charge_note = "..."
  |  + tag: reverse-charge       Add order tag
  +-------------------+
🇬🇷 Country Normalization
Greece uses GR in ISO 3166-1 but EL in the VIES system. Country codes are normalized before comparison: GR → EL. This normalization applies to both seller and buyer country codes for consistent evaluation.

EU Member States (27 + XI)

AT BE BG HR CY CZ DK EE FI FR DE EL HU IE IT LV LT LU MT NL PL PT RO SK SI ES SE XI

Integration Points

ORDERS_CREATE Webhook
Cart → Checkout complete
Order metafield + tag
Thank You save-vat
Post-checkout VAT submission
Order metafield + tag
🛒
Checkout Extension
Plus checkout VAT validation
Order metafield + tag
Merchant Configuration
Merchants can set their seller country code and customize the reverse charge note template via the admin dashboard. The note is written to the reverse_charge_note order metafield for invoice display.
9

Admin API Retry

Shopify Admin API calls are protected by a retry strategy that handles rate limits and transient failures with exponential backoff, up to 3 retries maximum.

Retry
429 Too Many Requests
Shopify GraphQL rate limit exceeded (40 req/s bucket)
Wait: Retry-After header
Backoff
5xx Server Error
Shopify infrastructure temporarily unavailable
Backoff: 2s, 4s, 8s
No Retry
4xx Client Error (except 429)
Request is malformed or unauthorized — retrying would not help
Fail immediately
ⓘ Usage Across Services
The retry wrapper is used in: order-metafield, customer-metafield, tax-exemption, and webhook handlers. Maximum 3 retries per operation. GraphQL rate limit: 40 requests per second.
10

Cart Attribute Security

Cart attributes are client-writable and can be manipulated via JavaScript. A defense-in-depth strategy combines client-side Function validation with server-side webhook re-validation.

cart-attribute-defense.diagram
           DEFENSE IN DEPTH: CART ATTRIBUTE VALIDATION

  +-------------------+         +-------------------+
  | Attacker           |         | Legitimate User   |
  |  JS console         |         |  VAT input widget  |
  |  manipulates attrs  |         |  validated value   |
  +--------+----------+         +--------+----------+
           |                              |
           v                              v
  +========================================================+
  |  LAYER 1: Shopify Function (cart validation)               |
  |  Format re-validation as basic protection               |
  |  Catches obvious manipulation (invalid format)         |
  +=============================+==========================+
                                |
                                v
  +========================================================+
  |  LAYER 2: ORDERS_CREATE Webhook (server-side)           |
  |  Full re-validation against VIES                       |
  |  Catches everything — authoritative validation         |
  +========================================================+
The Threat
Cart attributes are stored in Shopify's client-side cart object. Any JavaScript code running on the storefront can modify them.
  • Users can open browser console and modify attributes directly
  • Browser extensions or injected scripts could alter values
  • Attribute values cannot be trusted as-is
🛡
The Defense
Two layers ensure that manipulated values are caught before they affect tax calculations or order data.
  • Function: format regex check blocks obviously invalid values at checkout
  • Webhook: server-side VIES re-validation on order creation
  • Function catches fast; webhook catches everything
11

SEO / Indexing Protection

Prevents search engines from indexing or displaying VAT numbers entered by customers in search result snippets.

🌐
Widget Protection
  • data-nosnippet attribute on the VAT widget container
  • Prevents Google from using widget content in search snippets
  • Widget DOM content excluded from rich results
🔒
Input Protection
  • autocomplete="off" on VAT input field
  • Prevents browsers from caching VAT numbers in autofill
  • Reduces risk of VAT number leakage across sessions
vat-input.liquid
<!-- Prevent search engine indexing of VAT data -->
<div class="vat-widget"
     data-nosnippet>

  <input
    type="text"
    name="vat-number"
    autocomplete="off"
    placeholder="e.g. DE123456789"
  />

</div>