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.
08 — Security, Compliance & Privacy
Section titled “08 — Security, Compliance & Privacy”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).
0) Goal (one sentence)
Section titled “0) Goal (one sentence)”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.
1) Scope
Section titled “1) Scope”In scope
Section titled “In scope”| Blueprint bullet | Deliverable |
|---|---|
| Tokenized submission URLs with expiry | Schema + validation rules for TestimonialRequest (or token table); reject expired tokens in t.$token loader and in submit/upload-url APIs. |
| Rate limit public submission endpoints | api.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 version | TestimonialSubmission.consentAccepted, consentAcceptedAt, consentVersion aligned with Shop.testimonialConsentVersion (or campaign-level override if you add it later). |
| Moderation log immutable for audit | TestimonialModerationLog (§5.10): insert-only from server; no updates/deletes in app code; optional “reversal” = new log row, not edit. |
| GDPR webhooks | Extend app/routes/webhooks.compliance.jsx (or equivalent) to handle customer and shop data for testimonial tables: redact PII, export on data request if required. |
Out of scope (document only)
Section titled “Out of scope (document only)”- 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.).
2) Token security (submission links)
Section titled “2) Token security (submission links)”2.1 Entropy
Section titled “2.1 Entropy”submissionTokenmust be unguessable (minimum 128 bits entropy — e.g. UUID v4 or 32-byte hex). Never derive token from order id alone.
2.2 Expiry (recommended)
Section titled “2.2 Expiry (recommended)”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 inapp/lib/testimonial-constants.server.js).
Validation surfaces (must all agree):
t.$token.jsxloader — ifnow > tokenExpiresAt→ “link expired” page (generic copy).api.testimonial-upload-url— same check.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).
2.3 Transport
Section titled “2.3 Transport”- Production HTTPS only; reject mixed content in template guidance for 06 page assets.
2.4 Token in logs
Section titled “2.4 Token in logs”- Never log full
submissionTokenin application logs. If debug needed, log 8-char prefix +requestIdinternal id.
3) Rate limiting and abuse (public routes)
Section titled “3) Rate limiting and abuse (public routes)”3.1 Endpoints to protect
Section titled “3.1 Endpoints to protect”POST /api/testimonial-upload-url(or chosen path)POST /api/testimonial-submit- Optional:
GETpublic 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.
3.3 Suggested defaults (tune in env)
Section titled “3.3 Suggested defaults (tune in env)”- 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).
3.4 Response contract
Section titled “3.4 Response contract”- 429 with JSON
{ "error": "rate_limited", "retryAfterSec": 60 }andRetry-Afterheader when possible.
4) Consent and policy version
Section titled “4) Consent and policy version”4.1 Source of version string
Section titled “4.1 Source of version string”- Shop-level
testimonialConsentVersion(blueprint §5.1), e.g.2026-05-01or semantic1.0.0. - On 06 submit, copy current shop value into
TestimonialSubmission.consentVersionat save time (snapshot — do not update old rows when merchant bumps version).
4.2 UI (cross-reference 06)
Section titled “4.2 UI (cross-reference 06)”- Checkbox required; label must link to merchant’s published policy URL (config field on
Shopor static in v1). - Block submit if
consentAccepted !== trueserver-side (do not trust client only).
4.3 Withdrawal
Section titled “4.3 Withdrawal”- Product policy: “withdraw consent” = unpublish + redact or delete media per GDPR; treat as moderation + compliance action, not a silent DB delete without log.
5) Moderation audit log (immutability)
Section titled “5) Moderation audit log (immutability)”Model: TestimonialModerationLog — §5.10.
5.1 Rules
Section titled “5.1 Rules”- Create row on every moderation state change: approve, reject, archive, unpublish, feature, etc.
- No
updateordeleteonTestimonialModerationLogin application code. - If a decision is reversed (e.g. un-reject), insert a new log row with action
reinstateorapproveandreasonexplaining correction.
5.2 Who is the actor
Section titled “5.2 Who is the actor”actorType:merchant|system(auto-approve rules from Screen 9 usesystem).actorEmailfromauthenticate.adminsession when available.
5.3 Retention
Section titled “5.3 Retention”- Same retention as
TestimonialSubmissionunless 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:
| Entity | Typical PII / customer linkage |
|---|---|
TestimonialRequest | customerEmail, customerPhone, shopifyCustomerId, link to order |
TestimonialSubmission | displayEmail, displayName, shopifyCustomerId, review text |
SupportTicket / others | If 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.
6.2 customers/data_request
Section titled “6.2 customers/data_request”- 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.
6.3 customers/redact
Section titled “6.3 customers/redact”- Delete or anonymize rows matching customer identifiers:
- Null out
displayEmail, scrubdisplayNametoRedacted, 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-textreasonif it contains PII, or addredactedAt— decide with counsel; minimum: stop displaying PII in admin UI.
- Null out
6.4 shop/redact
Section titled “6.4 shop/redact”- After shop uninstall path, ensure testimonial extension data is removed; same as 04 cleanup alignment.
6.5 Idempotency
Section titled “6.5 Idempotency”- 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
storageKeypatterns in error messages. - Virus/malware: out of scope for v1; optional ClamAV later.
8) Admin and API surface
Section titled “8) Admin and API surface”- All merchant actions on submissions go through authenticated
authenticate.adminroutes; never trust?shop=alone for write. - Public GET testimonial API (05) must only return published content; never return
displayEmailor 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: falseon server. - Moderation: every approve/reject creates a new
TestimonialModerationLogrow; 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.
10) Implementation order (for Cursor)
Section titled “10) Implementation order (for Cursor)”- Add
tokenExpiresAt(if not present) + validation in 06 routes. - Implement rate limit helper + wire to upload + submit.
- Wire
consentVersionsnapshot on submit; confirmShopfield. - Enforce
TestimonialModerationLoginsert-only in moderation actions (when those screens exist). - Extend
webhooks.compliance.jsx(or splitwebhooks.compliance.testimonial.server.js) for testimonial tables; add integration tests with fixture payloads. - Document env vars:
CONSENT_POLICY_URLif used, rate limit numbers.
11) References
Section titled “11) References”02-Implementation-Blueprint.md— §8, §5.1, §5.5–5.8, §5.1001-Post-Purchase-Video-Testimonial-Collector-Plan.md— customer journey, consent- Shopify: Privacy law compliance (verify current URL in task)
12) Note on numbering
Section titled “12) Note on numbering”This folder already includes 05-…, 06-…, 07-…. This file is 08-….