Skip to content

08 — Security, Compliance & Privacy (Blueprint §8)

Cursor-ready plan: token safety, public endpoint abuse controls, consent versioning, immutable moderation audit, Shopify GDPR webhooks for testimonial data.

Source: 02-Implementation-Blueprint.md§8) Security and Compliance Requirements.

Product alignment: 01-Post-Purchase-Video-Testimonial-Collector-Plan.md — consent, privacy, merchant trust (collect only what you need).

This document is a build spec only. Do not implement until a task explicitly references this file.

Related plans: 06-public-submission-page-screen-13.md (public token page + submit APIs), 05-storefront-widgets-and-read-api.md (public read API), 07-email-sms-request-delivery-pipeline.md (requests hold customer contact).


The testimonial product protects submission tokens, limits abuse on public endpoints, records consent with version, maintains an append-only moderation audit trail, and honors Shopify GDPR compliance webhooks by redacting or exporting customer-linked testimonial data consistently.


Blueprint bulletDeliverable
Tokenized submission URLs with expirySchema + validation rules for TestimonialRequest (or token table); reject expired tokens in t.$token loader and in submit/upload-url APIs.
Rate limit public submission endpointsapi.testimonial-upload-url, api.testimonial-submit (and optionally public read API in 05): per-IP and per-token limits, consistent JSON errors.
Store consent + consent versionTestimonialSubmission.consentAccepted, consentAcceptedAt, consentVersion aligned with Shop.testimonialConsentVersion (or campaign-level override if you add it later).
Moderation log immutable for auditTestimonialModerationLog (§5.10): insert-only from server; no updates/deletes in app code; optional “reversal” = new log row, not edit.
GDPR webhooksExtend app/routes/webhooks.compliance.jsx (or equivalent) to handle customer and shop data for testimonial tables: redact PII, export on data request if required.
  • Legal review of consent copy (merchant + counsel).
  • SOC2 / ISO certification.
  • End-to-end encryption of media at rest (optional future); v1 can rely on provider defaults (S3 SSE, etc.).

  • submissionToken must be unguessable (minimum 128 bits entropy — e.g. UUID v4 or 32-byte hex). Never derive token from order id alone.

Blueprint requires expiry. Add to TestimonialRequest (migration):

  • tokenExpiresAt DateTime? @map("token_expires_at") — set when row is created, e.g. createdAt + 90 days (product decision; document constant in app/lib/testimonial-constants.server.js).

Validation surfaces (must all agree):

  1. t.$token.jsx loader — if now > tokenExpiresAt → “link expired” page (generic copy).
  2. api.testimonial-upload-url — same check.
  3. api.testimonial-submit — same check.

Open decision: whether expired status also blocks 07 reminder sends (it should — do not email dead links; set status to expired when appropriate).

  • Production HTTPS only; reject mixed content in template guidance for 06 page assets.
  • Never log full submissionToken in application logs. If debug needed, log 8-char prefix + requestId internal id.

3) Rate limiting and abuse (public routes)

Section titled “3) Rate limiting and abuse (public routes)”
  • POST /api/testimonial-upload-url (or chosen path)
  • POST /api/testimonial-submit
  • Optional: GET public testimonial list (05) to prevent scraping

3.2 Strategy (pick one and implement consistently)

Section titled “3.2 Strategy (pick one and implement consistently)”

Option A — In-process (dev/single instance): simple token bucket in memory per IP + per token (Map with TTL). Resets on deploy (acceptable for v1 on one dyno).

Option B — DB-backed: table RateLimitCounter(shopId, key, windowStart, count) with unique index; increment in transaction; cheap for serverless.

Option C — Edge / Redis: if infrastructure already has Redis, use fixed window counters.

  • Per IP: e.g. 60 upload-url requests / hour, 30 submits / hour (stricter for submit).
  • Per token: e.g. max 5 upload attempts / 15 minutes, 3 successful submits / day (second submit should 409 — 06 already covers duplicate).
  • 429 with JSON { "error": "rate_limited", "retryAfterSec": 60 } and Retry-After header when possible.

  • Shop-level testimonialConsentVersion (blueprint §5.1), e.g. 2026-05-01 or semantic 1.0.0.
  • On 06 submit, copy current shop value into TestimonialSubmission.consentVersion at save time (snapshot — do not update old rows when merchant bumps version).
  • Checkbox required; label must link to merchant’s published policy URL (config field on Shop or static in v1).
  • Block submit if consentAccepted !== true server-side (do not trust client only).
  • Product policy: “withdraw consent” = unpublish + redact or delete media per GDPR; treat as moderation + compliance action, not a silent DB delete without log.

Model: TestimonialModerationLog§5.10.

  • Create row on every moderation state change: approve, reject, archive, unpublish, feature, etc.
  • No update or delete on TestimonialModerationLog in application code.
  • If a decision is reversed (e.g. un-reject), insert a new log row with action reinstate or approve and reason explaining correction.
  • actorType: merchant | system (auto-approve rules from Screen 9 use system).
  • actorEmail from authenticate.admin session when available.
  • Same retention as TestimonialSubmission unless legal holds apply — document merchant-facing policy separately.

6) Shopify GDPR / privacy compliance webhooks

Section titled “6) Shopify GDPR / privacy compliance webhooks”

Shopify mandatory topics are already in shopify.app.toml (customers/data_request, customers/redact, shop/redact). Extend the existing compliance route handler to cover testimonial tenant data.

Reference implementation lives at app/routes/webhooks.compliance.jsx in this repo — read it before coding; mirror patterns for payload parsing, HMAC verification, and quick 200 responses.

6.1 Data inventory (map fields for export/redact)

Section titled “6.1 Data inventory (map fields for export/redact)”

Identify customer-linked columns across testimonial schema (§5.5–5.8), including but not limited to:

EntityTypical PII / customer linkage
TestimonialRequestcustomerEmail, customerPhone, shopifyCustomerId, link to order
TestimonialSubmissiondisplayEmail, displayName, shopifyCustomerId, review text
SupportTicket / othersIf retained (04 doc), same discipline

Shop redact: cascade delete (or explicit deletes in transaction) all shopId-scoped testimonial rows when Shopify sends shop/redact — rely on Prisma onDelete: Cascade from Shop where relations exist, or explicit $transaction deletes in dependency order.

  • Gather stored personal data for customer.id / email from webhook payload (exact shape per Shopify docs for your API version).
  • Return payload structure Shopify expects — follow existing app behavior; if the app only acknowledges and stores for manual export, document that; prefer automated JSON if API allows.
  • Delete or anonymize rows matching customer identifiers:
    • Null out displayEmail, scrub displayName to Redacted, optionally delete media blobs from storage + rows for submissions attributed to that customer.
    • TestimonialModerationLog: do not delete audit rows if legally required to retain actions — instead anonymize free-text reason if it contains PII, or add redactedAtdecide with counsel; minimum: stop displaying PII in admin UI.
  • After shop uninstall path, ensure testimonial extension data is removed; same as 04 cleanup alignment.
  • Webhook retries are normal; redact handlers must be idempotent (second redact = no-op success).

7) Storage and media security (minimum bar)

Section titled “7) Storage and media security (minimum bar)”
  • Presigned URLs: short TTL for upload; private bucket; public playback URLs only after processing and publish approval (or signed playback — product choice).
  • No public list of storageKey patterns in error messages.
  • Virus/malware: out of scope for v1; optional ClamAV later.

  • All merchant actions on submissions go through authenticated authenticate.admin routes; never trust ?shop= alone for write.
  • Public GET testimonial API (05) must only return published content; never return displayEmail or internal notes.

9) Testing checklist (acceptance criteria)

Section titled “9) Testing checklist (acceptance criteria)”
  • Expired token: all three surfaces (page, upload-url, submit) return consistent failure.
  • Rate limit: 429 after threshold; no DB corruption.
  • Consent: cannot submit with consentAccepted: false on server.
  • Moderation: every approve/reject creates a new TestimonialModerationLog row; no updates to old log rows in code paths.
  • customers/redact test payload: customer-linked fields cleared; app returns 200; second call idempotent.
  • shop/redact: all testimonial data for shop removed or verified cascade.
  • No full tokens in server logs in default log level.

  1. Add tokenExpiresAt (if not present) + validation in 06 routes.
  2. Implement rate limit helper + wire to upload + submit.
  3. Wire consentVersion snapshot on submit; confirm Shop field.
  4. Enforce TestimonialModerationLog insert-only in moderation actions (when those screens exist).
  5. Extend webhooks.compliance.jsx (or split webhooks.compliance.testimonial.server.js) for testimonial tables; add integration tests with fixture payloads.
  6. Document env vars: CONSENT_POLICY_URL if used, rate limit numbers.

  • 02-Implementation-Blueprint.md§8, §5.1, §5.5–5.8, §5.10
  • 01-Post-Purchase-Video-Testimonial-Collector-Plan.md — customer journey, consent
  • Shopify: Privacy law compliance (verify current URL in task)

This folder already includes 05-…, 06-…, 07-…. This file is 08-….