SOP-PROOF-01: Proof Portal System¶
Document Status: PRODUCTION READY
Version: 1.0
Last Updated: 2026-01-27
Owner: Operations
Classification: Internal Reference
1. Overview¶
1.1 Purpose¶
The Proof Portal is Protocol Raw's public-facing verification system. It allows customers, vets, and prospects to verify the safety testing results for any released batch. The portal serves as the operational manifestation of "Proof, Not Promises" — transforming trust claims into verifiable evidence.
1.2 Strategic Role¶
The Proof Portal is not a marketing asset. It is:
- A verification tool — Customers confirm their batch passed testing
- A trust-transfer mechanism — Customers share proof with vets and family
- A competitive moat — Time-locked trust capital that compounds with each batch
- A differentiation signal — Proves Protocol Raw operates differently than competitors
1.3 Access Points¶
| Entry Point | URL | Use Case |
|---|---|---|
| QR Code Scan | proof.protocolraw.co.uk/batch/{id} |
Customer scans packaging |
| Portal Homepage | proof.protocolraw.co.uk |
Manual batch search |
| Shared Link | proof.protocolraw.co.uk/batch/{id} |
Vet/family receives link |
| Website Link | protocolraw.co.uk/proof |
Prospect exploration |
2. System Architecture¶
2.1 Technical Stack¶
| Component | Technology | Purpose |
|---|---|---|
| Edge Function | Supabase Deno | Server-side rendering, API endpoints |
| Database | Supabase PostgreSQL | Batch data, lab results, analytics |
| Domain | Cloudflare | DNS, SSL, caching |
| Fonts | Google Fonts | Inter, Montserrat |
2.2 Edge Function: proof-page¶
Location: Supabase Edge Functions
Schema: raw_ops
Routes¶
| Route | Method | Handler | Purpose |
|---|---|---|---|
/ |
GET | servePortalHomepage() |
Portal homepage with search |
/batch |
GET | servePortalHomepage() |
Alias for homepage |
/batch/{public_batch_id} |
GET | serveBatchProofPage() |
Individual batch page |
/api/search |
GET | handleBatchSearch() |
Batch lookup API |
/api/event |
POST | handleEventTracking() |
Analytics events |
?batch={id} |
GET | Legacy redirect | Backwards compatibility |
2.3 Database Schema¶
View: v_released_batches_verified¶
Source view that aggregates batch and lab result data for released batches only.
| Column | Type | Description |
|---|---|---|
id |
UUID | Batch primary key |
batch_code |
TEXT | Human-readable batch ID (e.g., TEST-BATCH-004) |
public_batch_id |
TEXT | URL-safe identifier (e.g., PR-2CCF573F) |
product_id |
UUID | FK to products table |
formulation_id |
UUID | FK to formulations table |
produced_at |
TIMESTAMPTZ | Production date |
reported_at |
TIMESTAMPTZ | Lab report date |
salmonella_absent |
BOOLEAN | TRUE = not detected |
listeria_absent |
BOOLEAN | TRUE = not detected |
enterobacteriaceae_cfu_per_g |
INTEGER | Colony count |
lab_name |
TEXT | Testing laboratory name |
report_pdf_url |
TEXT | Link to original certificate |
Table: proof_page_events¶
Analytics tracking for proof portal engagement.
CREATE TABLE raw_ops.proof_page_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_type TEXT NOT NULL,
batch_code TEXT NOT NULL,
public_batch_id TEXT,
referrer TEXT,
user_agent TEXT,
device_type TEXT,
share_method TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Indexes
CREATE INDEX idx_proof_page_events_type_created
ON raw_ops.proof_page_events(event_type, created_at DESC);
CREATE INDEX idx_proof_page_events_batch
ON raw_ops.proof_page_events(batch_code, created_at DESC);
-- Permissions
GRANT INSERT, SELECT ON raw_ops.proof_page_events TO anon;
3. Portal Homepage¶
3.1 URL¶
3.2 Purpose¶
Allow customers to manually search for their batch if they don't have the direct QR link.
3.3 Page Structure¶
Hero Section (Stone background + dot texture)¶
Headline:
Subtitle:
This is our searchable database of independent test results. Enter the Batch ID found on your packaging to view its safety certificate.
Search Card: - Input field: Accepts batch ID (auto-uppercases) - Button: "Verify Batch" - States: Default → Loading → Success/Error/Info - Hint: "Find your Batch ID on the QR code label"
Latest Batch Showcase: - Displays most recent released batch - Shows test results summary - Links to full batch page
Process Timeline Section (Cream background)¶
Headline:
Subtitle:
Steps: 1. Batch Produced — Food blended, portioned, frozen with unique ID 2. Independent Testing — Sample sent to UKAS-accredited lab 3. Results Received — Lab tests for Salmonella, Listeria, hygiene indicators 4. Verified & Released — Only passing batches ship
CTA Section (Stone background + dot texture)¶
Headline:
Body:
CTAs: - Primary: "Calculate Portion" → /calculator - Secondary: "How It Works" → /how-it-works
3.4 Search API¶
Endpoint: GET /api/search?batch_code={id}
Response:
// Found and released
{ "found": true, "status": "RELEASED", "public_batch_id": "PR-2CCF573F" }
// Found but not released
{ "found": true, "status": "NOT_RELEASED" }
// Not found
{ "found": false }
4. Individual Batch Page¶
4.1 URL¶
4.2 Purpose¶
Display complete verification data for a specific batch. This is the page customers land on from QR codes and shared links.
4.3 Page Structure¶
Header Section¶
Brand:
Badge:
(Forest Green, animated glow after reveal)Headline:
Intro Copy:
This is your proof that your dog's food is safe. This batch was tested for Salmonella, Listeria, and Enterobacteriaceae before release.
You can feed your dog with confidence.
Batch Info Card: | Label | Value | |-------|-------| | BATCH ID | {batch_code} | | TESTED | {reported_at} | | STATUS | Verified Safe |
Share Actions: - "Send to My Vet" (Forest Green button) - "Share This Proof" (White outline button) - Hint: "Useful if your vet or family has questions."
Lab Analysis Section¶
Heading:
Test Card 1: Salmonella | Field | Value | |-------|-------| | Test Name | Pathogen Test: Salmonella | | Badge | PASS (green circle) | | Result | Absent in 25g | | Standard | Absent in 25g | | Explanation | No Salmonella was detected in the tested sample. |
Test Card 2: Listeria | Field | Value | |-------|-------| | Test Name | Pathogen Test: Listeria monocytogenes | | Badge | PASS/PENDING/FAIL | | Result | Absent in 25g | | Standard | Absent in 25g | | Explanation | No Listeria was detected in the tested sample. |
Test Card 3: Enterobacteriaceae | Field | Value | |-------|-------| | Test Name | Hygiene Indicator: Enterobacteriaceae | | Badge | PASS/PENDING | | Result | {value} cfu/g | | Standard | <100 cfu/g | | Explanation | Low hygiene indicator confirms good manufacturing and cold-chain control. |
Safe and Complete Section¶
Heading:
Body:
CTA: "View Formulation Analysis" (conditional, if URL exists)
Lab Certificate Section¶
Heading:
Body:
We translate the scientific report into the clear summary above. For complete transparency, you can also view the original certificate issued by the lab.
CTA: "Download Lab Certificate (PDF)" (conditional, if URL exists)
Footer¶
Background: Espresso with dot texture
Text:
5. Share Functionality¶
5.1 Strategic Purpose¶
Share buttons are a growth primitive, not a marketing feature. They enable customers to transfer trust to skeptical audiences (vets, family) using verifiable evidence rather than brand claims.
5.2 Send to My Vet¶
Trigger: Button click
Action: Opens mailto with prefilled content
Subject:
Body:
Hi,
I wanted to share the independent pathogen test results for the raw food I'm feeding my dog.
This batch was tested before release for:
- Salmonella
- Listeria
- Enterobacteriaceae (hygiene indicator)
The full lab report is published here:
{batch_url}
I'm sharing this because safety is the main concern I've heard raised around raw diets, and I found this level of transparency helpful when deciding what to feed.
Best regards
Design Rationale: - Clinical framing ("pathogen test results") speaks vet language - Bullet list is scannable - States why sharing without asking for validation - No marketing language
5.3 Share This Proof¶
Trigger: Button click
Action: Web Share API (mobile) or copy to clipboard (desktop)
Share Data:
{
title: 'Batch {batch_code} Lab Results',
text: 'My dog\'s food is tested for Salmonella and Listeria before it\'s released. You can see the lab results here:',
url: '{batch_url}'
}
Design Rationale: - Names specific pathogens (not abstract "safety") - "Before it's released" = Hold-and-Release without explaining it - First person ("my dog's food") = non-preachy - No emojis, no "Verified Safe" badge text
5.4 Share Hint¶
Text:
Purpose: Frames sharing as practical, not promotional. Gives permission without pressure.
6. Analytics¶
6.1 Events Tracked¶
| Event | Trigger | Properties |
|---|---|---|
proof_page_view |
Page load | batch_code, public_batch_id, referrer, device_type |
send_to_vet_click |
Vet button clicked | batch_code, share_method: 'email' |
share_proof_click |
Share button clicked | batch_code, share_method: 'native' or 'copy' |
6.2 Tracking Implementation¶
function trackEvent(eventType, extraData = {}) {
const payload = {
event_type: eventType,
batch_code: batchCode,
public_batch_id: publicBatchId,
referrer: document.referrer || null,
device_type: window.innerWidth <= 768 ? 'mobile' : 'desktop',
...extraData
};
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/event', JSON.stringify(payload));
} else {
fetch('/api/event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
keepalive: true
}).catch(() => {});
}
}
6.3 Key Metrics¶
| Metric | Query | Insight |
|---|---|---|
| Daily page views | COUNT(*) WHERE event_type = 'proof_page_view' AND created_at > now() - interval '1 day' |
QR scan engagement |
| Share rate | share_clicks / page_views |
Trust transfer activity |
| Vet share rate | vet_clicks / page_views |
Professional validation seeking |
| Mobile vs Desktop | GROUP BY device_type |
Access context |
| Referrer breakdown | GROUP BY referrer |
Entry point analysis |
6.4 Metabase Dashboard Queries¶
Daily Views:
SELECT
DATE(created_at) as date,
COUNT(*) as views
FROM raw_ops.proof_page_events
WHERE event_type = 'proof_page_view'
GROUP BY DATE(created_at)
ORDER BY date DESC
LIMIT 30;
Share Funnel:
SELECT
event_type,
COUNT(*) as count
FROM raw_ops.proof_page_events
WHERE created_at > now() - interval '30 days'
GROUP BY event_type
ORDER BY count DESC;
Share Method Breakdown:
SELECT
share_method,
COUNT(*) as count
FROM raw_ops.proof_page_events
WHERE event_type = 'share_proof_click'
AND created_at > now() - interval '30 days'
GROUP BY share_method;
7. Visual Design¶
7.1 Design System Reference¶
Source: Visual Identity Guide v2.2
7.2 Color Palette¶
| Token | Hex | Usage |
|---|---|---|
--col-espresso |
#2B2523 | Primary text, footer |
--col-forest-green |
#2D5144 | Pass badges, verified status, share buttons |
--col-burnt-sienna |
#B85C3A | Primary CTAs |
--col-cream |
#F9F7F4 | Body background, header gradient |
--col-warm-linen |
#EBE8E3 | Card backgrounds, borders |
--col-taupe |
#6B6360 | Secondary text, labels |
--col-amber |
#D4A574 | Pending badges |
Batch Page Container: Pure white (#FFFFFF) for clinical clarity
7.3 Typography¶
| Element | Font | Weight | Size (Desktop) | Size (Mobile) |
|---|---|---|---|---|
| Brand header | Montserrat | 700 | 18px | 18px |
| H1 | Montserrat | 700 | 48px | 32px |
| H2 | Montserrat | 700 | 28px | 22px |
| Body | Inter | 400 | 18px | 16px |
| Labels | Inter | 600 | 10-11px | 10-11px |
| Buttons | Inter | 600 | 14-15px | 14px |
7.4 Animations¶
All animations respect prefers-reduced-motion: reduce.
| Element | Animation | Duration | Delay |
|---|---|---|---|
| Container | Fade up + scale | 1000ms | 100ms |
| Verified badge | Bounce in | 700ms | 500ms |
| Badge glow | Continuous pulse | 3000ms | After reveal |
| Test cards | Staggered slide up | 400ms | 0/150/300ms |
| Progress rings | Stroke draw | 1200ms | On viewport |
| Status dot | Pulse | 2000ms | Continuous |
7.5 Responsive Breakpoints¶
| Breakpoint | Changes |
|---|---|
| ≤640px | Single column layout, stacked share buttons, smaller headings |
| >640px | 3-column batch info, side-by-side share buttons |
8. Security¶
8.1 Content Security Policy¶
default-src 'self';
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data: https:;
script-src 'self' 'unsafe-inline';
frame-ancestors 'none';
base-uri 'self';
form-action 'self'
8.2 Additional Headers¶
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=(), camera=()
8.3 Data Access¶
- Only RELEASED batches are visible via
v_released_batches_verified - Batches in testing/hold status return "NOT_RELEASED" with no details
- No PII is exposed through the proof portal
9. Caching¶
| Page | Cache-Control | Rationale |
|---|---|---|
| Batch pages | public, max-age=3600, stale-while-revalidate=86400 |
Lab results don't change once released |
| Portal homepage | public, max-age=300 |
Latest batch updates more frequently |
| API endpoints | No cache | Real-time data |
10. Copy Principles¶
10.1 Voice Guidelines¶
Aligned with: Protocol Raw Brand Voice & Copy Guidelines v1.0
- Show, don't tell — Let lab results speak
- Systematic, not emotional — Facts over reassurance
- Clinical where appropriate — Vets are an audience
- No anxiety amplification — Don't mention risks at relief moments
10.2 Key Copy Decisions¶
| Element | Decision | Rationale |
|---|---|---|
| Badge text | "Verified Safe" not "Verified & Released" | Customer language, not internal |
| Intro copy | Names pathogens upfront | Address the fear directly |
| Test explanations | Single sentence, no "What this means:" | Trust readers, don't over-explain |
| Listeria card | No mention of illness severity | Don't spike anxiety at relief moment |
| FEDIAF claim | "Formulated to meet" not "independently verified" | Only claim verification when certificate exists |
| Social share | Names pathogens, mentions "before release" | Specific, not abstract |
| Vet email | Clinical tone, bullet points | Vets want data, not marketing |
11. Operational Procedures¶
11.1 Batch Release Flow¶
1. Batch produced → status = 'PRODUCED'
2. Sample sent to lab → status = 'TESTING'
3. Results received → lab_results record created
4. QC review → status = 'RELEASED' (if pass)
5. Proof page becomes accessible
11.2 Lab Result Integration¶
Lab results are parsed from PDF certificates using GPT-4o Vision and stored in lab_results table. See SOP-0X for lab result processing.
11.3 Monitoring¶
Alert Triggers: - Proof page error rate > 1% - API endpoint latency > 2s - Analytics insert failures
Dashboard: Metabase proof portal dashboard (to be created)
12. Future Enhancements¶
12.1 Planned¶
| Enhancement | Priority | Status |
|---|---|---|
| Metabase dashboard | High | Pending |
| Download certificate tracking | Medium | Not started |
| Formulation link tracking | Medium | Not started |
| A/B test share copy | Low | Not started |
12.2 Considered but Deferred¶
| Enhancement | Reason Deferred |
|---|---|
| Social proof counters | Could feel like marketing |
| Comments/reviews | Out of scope for verification tool |
| Batch comparison | Low value, adds complexity |
13. Related Documents¶
| Document | Relationship |
|---|---|
| Visual Identity Guide v2.2 | Design system source |
| Protocol Raw Brand Voice Guidelines v1.0 | Copy voice reference |
| SOP-0X Lab Result Processing | Upstream data flow |
| SOP-01 Batch Lifecycle | Batch status management |
14. Version History¶
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2026-01-27 | Claude | Initial comprehensive documentation |
Appendix A: Complete Edge Function Route Handlers¶
A.1 servePortalHomepage()¶
Fetches latest released batch and renders homepage HTML.
A.2 serveBatchProofPage()¶
Fetches batch by public_batch_id from v_released_batches_verified, joins product and formulation data, renders batch page HTML.
A.3 handleBatchSearch()¶
Looks up batch by public_batch_id, returns JSON with found/status/public_batch_id.
A.4 handleEventTracking()¶
Validates event_type, inserts to proof_page_events table, returns success/error JSON.
Appendix B: Sample Analytics Queries¶
B.1 Weekly Share Report¶
SELECT
DATE_TRUNC('week', created_at) as week,
COUNT(*) FILTER (WHERE event_type = 'proof_page_view') as views,
COUNT(*) FILTER (WHERE event_type = 'send_to_vet_click') as vet_shares,
COUNT(*) FILTER (WHERE event_type = 'share_proof_click') as social_shares,
ROUND(
COUNT(*) FILTER (WHERE event_type IN ('send_to_vet_click', 'share_proof_click'))::numeric /
NULLIF(COUNT(*) FILTER (WHERE event_type = 'proof_page_view'), 0) * 100,
2
) as share_rate_pct
FROM raw_ops.proof_page_events
WHERE created_at > now() - interval '12 weeks'
GROUP BY DATE_TRUNC('week', created_at)
ORDER BY week DESC;