# pfpmaker.online backend / data / Cloudflare architecture contract

- Date: 2026-06-03
- Tenant: site-pfpmaker-20260603
- Task: t_6515084d
- Owner: moshu
- Domain: pfpmaker.online
- Project directory expected by pipeline: /root/projects/pfpmaker
- Upstream inputs:
  - /root/.hermes/reports/site-pfpmaker-20260603/research-v0.md
  - /root/.hermes/reports/site-pfpmaker-20260603/prd-v1.md
  - /root/.hermes/reports/site-pfpmaker-20260603/page-matrix.md
  - /root/.hermes/reports/site-pfpmaker-20260603/pricing-model.md
  - /root/.hermes/reports/site-pfpmaker-20260603/compliance-boundary.md
  - /root/.hermes/reports/site-pfpmaker-20260603/design-acceptance.md

## 0. Decision

MVP backend verdict: STATIC_FIRST_GO.

pfpmaker.online P0 should ship as a Cloudflare Pages static site with browser-side image processing. No account system, no Stripe, no entitlement DB, no server-side image upload, no R2 image storage, and no AI provider call are required for MVP.

Recommended P0 runtime:

```json
{
  "project_slug": "pfpmaker",
  "domain": "pfpmaker.online",
  "backend_verdict": "STATIC_FIRST_GO",
  "runtime": "Cloudflare Pages static deploy",
  "core_editor_processing": "browser_local_canvas_or_equivalent",
  "workers_required_p0": false,
  "d1_required_p0": false,
  "r2_required_p0": false,
  "kv_required_p0": false,
  "queues_required_p0": false,
  "auth_required_p0": false,
  "stripe_required_p0": false,
  "ai_provider_required_p0": false,
  "checkout_enabled_plans": [],
  "image_upload_to_server_allowed_p0": false,
  "launch_condition": "front-end must verify upload/edit/download without network image upload before using local-first privacy copy"
}
```

Why: PRD and pricing both define P0 as no-signup, no-watermark, free browser-side editor. Compliance requires local-first image handling unless Privacy is upgraded. Design acceptance is conditional GO but requires replacing risky placeholder imagery. Therefore P0 backend scope is mostly data contract + Cloudflare deployment/security + analytics boundary, not D1/R2/Stripe/OAuth implementation.

## 1. Architecture

### 1.1 P0 architecture

```text
User browser
  -> Cloudflare Pages static assets
  -> React/Canvas editor in browser
  -> local File API reads JPG/PNG/WebP
  -> local crop/resize/background/border/text rendering
  -> local PNG/WebP download

Optional analytics only
  -> Cloudflare Web Analytics / Plausible event endpoint
  -> event metadata only: route, preset, style option, export format, success/failure
  -> never image content, file name, image hash, face attributes, or image URL
```

P0 should not create upload APIs. The browser must not POST original images, edited canvases, thumbnails, face crops, file names, image hashes, or generated object URLs to Workers, R2, AI providers, analytics, logs, or third-party services.

### 1.2 Cloudflare resources

| Resource | P0 decision | Binding | Reason |
|---|---|---|---|
| Cloudflare Pages | Required | static assets | Host pages, editor bundle, legal pages, sitemap/robots |
| Workers / Pages Functions | Not required for core P0 | none | No backend API needed if no waitlist/contact form is implemented |
| D1 | Not required for core P0 | none | No users/orders/credits/history in MVP |
| R2 | Not required and should not be used for P0 images | none | Compliance wants no server-side image storage |
| KV | Not required | none | Static config can be bundled as versioned JSON/TS |
| Queues / DO | Not required | none | No async AI or upload jobs |
| Cloudflare Web Analytics | Optional | dashboard script | Privacy-friendly page/event analytics if configured without image data |
| Email Routing | Recommended before launch | support@pfpmaker.online aliases | Contact/legal complaint/privacy request channel |

### 1.3 If a form is added

If the front-end wants “Join Pro Waitlist” or contact form in MVP, implement the smallest dynamic surface separately from image editing:

- Route: `POST /api/waitlist`
- Runtime: Pages Function or Worker
- Storage: D1 table `waitlist_subscribers`
- Scope: email + intent + source route only
- Still no image upload

D1 schema for optional waitlist:

```sql
CREATE TABLE IF NOT EXISTS waitlist_subscribers (
  id TEXT PRIMARY KEY,
  email TEXT NOT NULL UNIQUE,
  email_domain TEXT,
  intent TEXT NOT NULL DEFAULT 'pro_waitlist',
  source_path TEXT NOT NULL DEFAULT '/',
  consent_text TEXT NOT NULL,
  status TEXT NOT NULL DEFAULT 'active',
  first_subscribed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
  last_submitted_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
  submit_count INTEGER NOT NULL DEFAULT 1,
  user_agent TEXT,
  country TEXT,
  referrer TEXT
);

CREATE INDEX IF NOT EXISTS idx_waitlist_status_intent
  ON waitlist_subscribers(status, intent);

CREATE INDEX IF NOT EXISTS idx_waitlist_last_submitted
  ON waitlist_subscribers(last_submitted_at DESC);
```

Do not add auth/payment tables until P1 paid features are real.

## 2. API contract

### 2.1 P0 required APIs

None.

The core editor must work fully without `/api/*`.

### 2.2 Optional P0 analytics events

Analytics events may be client-only provider calls or routed through a minimal Worker later. Payload must stay non-image and low cardinality.

```ts
type AnalyticsEventName =
  | 'page_view'
  | 'tool_start'
  | 'upload_success'
  | 'upload_error'
  | 'preset_select'
  | 'style_change'
  | 'download_click'
  | 'download_success'
  | 'download_error'
  | 'faq_click'
  | 'related_page_click'
  | 'future_pro_click'
  | 'waitlist_submit';

type AnalyticsPayload = {
  event: AnalyticsEventName;
  route: '/' | '/discord-pfp-maker' | '/profile-picture-maker' | '/ai-pfp-maker' | '/templates';
  preset?: PlatformPresetId;
  template_id?: TemplateId;
  export_format?: 'png' | 'webp';
  export_size_px?: 512 | 1024 | 2048;
  style_flags?: Array<'solid_background' | 'gradient_background' | 'blur_background' | 'transparent_background' | 'border' | 'outline' | 'shadow' | 'text_ring'>;
  error_code?: 'unsupported_file_type' | 'file_too_large' | 'canvas_export_failed' | 'browser_unsupported';
};
```

Forbidden analytics fields:

- file name
- image URL / blob URL
- image hash / perceptual hash
- EXIF
- face box / landmarks / age / gender / ethnicity / beauty score
- user-entered custom text if it can identify a person or account
- full IP address in app payload

### 2.3 Optional `POST /api/waitlist`

Only implement if copy/design includes a real waitlist form.

Request:

```json
{
  "email": "user@example.com",
  "intent": "pro_waitlist",
  "source_path": "/ai-pfp-maker",
  "consent_text": "Notify me about future Pro/AI tools for PFP Maker Online."
}
```

Response:

```json
{
  "ok": true,
  "status": "subscribed"
}
```

Validation:

- email required, normalized lowercase, max 254 chars
- intent enum: `pro_waitlist`, `team_access`, `ai_styles`, `contact`
- source_path allowlist from page matrix
- id generated server-side with nanoid/crypto random UUID
- duplicate email updates intent/source/status/last_submitted_at and increments submit_count
- rate limit by IP/session if form abuse appears

## 3. Static data contract

The seed should live in the front-end repo as versioned static data, e.g.:

```text
src/data/pfp-platform-presets.ts
src/data/pfp-templates.ts
src/data/pfp-page-config.ts
```

Every record must expose `source`, `confidence`, and `updated` so QA/SEO can distinguish verified guidance from provisional product heuristics.

### 3.1 Types

```ts
type Confidence = 'confirmed' | 'source_backed' | 'heuristic' | 'needs_verification';

type SourceRef = {
  label: string;
  url?: string;
  note?: string;
};

type PlatformPreset = {
  id: PlatformPresetId;
  label: string;
  route_targets: string[];
  recommended_export: {
    width: number;
    height: number;
    format_default: 'png' | 'webp';
    format_options: Array<'png' | 'webp'>;
  };
  preview: {
    shape: 'circle' | 'rounded_square' | 'square';
    safe_area_ratio: number;
    show_safe_area: boolean;
  };
  guidance: string[];
  caveat: string;
  source: SourceRef[];
  confidence: Confidence;
  updated: string;
  no_affiliation_required: boolean;
};

type TemplateSeed = {
  id: TemplateId;
  name: string;
  category: 'minimal' | 'gradient' | 'gaming' | 'creator' | 'professional' | 'dark' | 'pastel' | 'discord';
  index_ready: boolean;
  apply_ready: boolean;
  preview_asset: string | null;
  config: {
    background: { type: 'solid' | 'gradient' | 'blur' | 'transparent'; value: string };
    border?: { color: string; width: number; opacity?: number };
    shadow?: { color: string; blur: number; opacity: number };
    text_ring?: { enabled: boolean; default_text?: string };
  };
  provenance: 'original_css_canvas' | 'licensed_asset' | 'placeholder';
  license: string;
  source: SourceRef[];
  confidence: Confidence;
  updated: string;
};
```

### 3.2 Platform presets seed

P0 product should export square images and use circle previews for most avatar platforms. Exact platform requirements change, so UI copy must say “commonly used / recommended guidance” and not “officially approved”.

```json
[
  {
    "id": "discord",
    "label": "Discord",
    "route_targets": ["/", "/discord-pfp-maker"],
    "recommended_export": { "width": 512, "height": 512, "format_default": "png", "format_options": ["png", "webp"] },
    "preview": { "shape": "circle", "safe_area_ratio": 0.82, "show_safe_area": true },
    "guidance": ["Keep faces/logos centered", "Leave margin for circular display", "Use PNG for crisp text or logos"],
    "caveat": "Discord display rules may change; this preset is sizing guidance, not official approval.",
    "source": [
      { "label": "Upstream PRD P0 preset list", "note": "Required platform preset" },
      { "label": "Search result sample", "url": "https://www.pixazo.ai/blog/discord-profile-picture-size", "note": "Search result describes 128x128 as Discord recommendation; product exports larger square for quality." }
    ],
    "confidence": "source_backed",
    "updated": "2026-06-03",
    "no_affiliation_required": true
  },
  {
    "id": "instagram",
    "label": "Instagram",
    "route_targets": ["/"],
    "recommended_export": { "width": 1080, "height": 1080, "format_default": "png", "format_options": ["png", "webp"] },
    "preview": { "shape": "circle", "safe_area_ratio": 0.82, "show_safe_area": true },
    "guidance": ["Use a centered square image", "Avoid important details near corners", "Check small-size readability"],
    "caveat": "Platform names are descriptive only; not affiliated with Instagram or Meta.",
    "source": [
      { "label": "Upstream PRD P0 preset list", "note": "Required platform preset" },
      { "label": "Search result sample", "url": "https://taplink.at/en/blog/instagram-profile-picture-size.html", "note": "Search result says 1080x1080 is a good Instagram profile picture size in 2026." }
    ],
    "confidence": "source_backed",
    "updated": "2026-06-03",
    "no_affiliation_required": true
  },
  {
    "id": "tiktok",
    "label": "TikTok",
    "route_targets": ["/"],
    "recommended_export": { "width": 512, "height": 512, "format_default": "png", "format_options": ["png", "webp"] },
    "preview": { "shape": "circle", "safe_area_ratio": 0.82, "show_safe_area": true },
    "guidance": ["Keep the subject centered", "Avoid text near the edge", "Use high contrast for mobile display"],
    "caveat": "TikTok requirements may change; do not use TikTok logos or imply endorsement.",
    "source": [
      { "label": "Upstream PRD P0 preset list", "note": "Required platform preset" },
      { "label": "Hootsuite social media image size guide search result", "url": "https://blog.hootsuite.com/social-media-image-sizes-guide", "note": "Search result mentions TikTok profile photo 200x200 recommended; product exports larger square for quality." }
    ],
    "confidence": "source_backed",
    "updated": "2026-06-03",
    "no_affiliation_required": true
  },
  {
    "id": "youtube",
    "label": "YouTube",
    "route_targets": ["/"],
    "recommended_export": { "width": 800, "height": 800, "format_default": "png", "format_options": ["png", "webp"] },
    "preview": { "shape": "circle", "safe_area_ratio": 0.82, "show_safe_area": true },
    "guidance": ["Use square channel profile artwork", "Keep logo/face in center", "Preview at small sizes"],
    "caveat": "YouTube profile display can vary by surface; this is guidance, not official approval.",
    "source": [
      { "label": "Upstream PRD P0 preset list", "note": "Required platform preset" },
      { "label": "Search result sample", "url": "https://www.pixazo.ai/blog/youtube-profile-picture-size-guide", "note": "Search result describes 800x800 as YouTube profile picture recommendation." }
    ],
    "confidence": "source_backed",
    "updated": "2026-06-03",
    "no_affiliation_required": true
  },
  {
    "id": "x",
    "label": "X",
    "route_targets": ["/"],
    "recommended_export": { "width": 512, "height": 512, "format_default": "png", "format_options": ["png", "webp"] },
    "preview": { "shape": "circle", "safe_area_ratio": 0.82, "show_safe_area": true },
    "guidance": ["Use a centered square avatar", "Avoid small text", "Check circle crop"],
    "caveat": "Needs final source verification before claiming exact platform size.",
    "source": [{ "label": "Upstream PRD P0 preset list", "note": "Required platform preset; exact platform size not independently verified in this task." }],
    "confidence": "needs_verification",
    "updated": "2026-06-03",
    "no_affiliation_required": true
  },
  {
    "id": "linkedin",
    "label": "LinkedIn",
    "route_targets": ["/", "/profile-picture-maker"],
    "recommended_export": { "width": 800, "height": 800, "format_default": "png", "format_options": ["png", "webp"] },
    "preview": { "shape": "circle", "safe_area_ratio": 0.82, "show_safe_area": true },
    "guidance": ["Use a clean centered profile image", "Avoid promising professional headshots", "Keep background simple"],
    "caveat": "Needs final source verification before claiming exact platform size; do not imply LinkedIn approval.",
    "source": [{ "label": "Upstream PRD P0 preset list", "note": "Required platform preset; exact platform size not independently verified in this task." }],
    "confidence": "needs_verification",
    "updated": "2026-06-03",
    "no_affiliation_required": true
  },
  {
    "id": "reddit",
    "label": "Reddit",
    "route_targets": ["/"],
    "recommended_export": { "width": 512, "height": 512, "format_default": "png", "format_options": ["png", "webp"] },
    "preview": { "shape": "circle", "safe_area_ratio": 0.82, "show_safe_area": true },
    "guidance": ["Use simple centered artwork", "Check small preview readability", "Avoid copyrighted community logos unless owned"],
    "caveat": "Needs final source verification before claiming exact platform size.",
    "source": [{ "label": "Upstream PRD P0 preset list", "note": "Required platform preset; exact platform size not independently verified in this task." }],
    "confidence": "needs_verification",
    "updated": "2026-06-03",
    "no_affiliation_required": true
  },
  {
    "id": "twitch",
    "label": "Twitch",
    "route_targets": ["/"],
    "recommended_export": { "width": 512, "height": 512, "format_default": "png", "format_options": ["png", "webp"] },
    "preview": { "shape": "circle", "safe_area_ratio": 0.82, "show_safe_area": true },
    "guidance": ["Use centered creator or channel artwork", "Leave margin for circle crop", "Avoid platform marks unless authorized"],
    "caveat": "Needs final source verification before claiming exact platform size.",
    "source": [{ "label": "Upstream PRD P0 preset list", "note": "Required platform preset; exact platform size not independently verified in this task." }],
    "confidence": "needs_verification",
    "updated": "2026-06-03",
    "no_affiliation_required": true
  }
]
```

Implementation rule: UI may show exact export dimensions from the record, but platform copy must keep caveats. For `needs_verification` presets, do not write “recommended by X” or “official size”.

### 3.3 Template seed contract

`/templates` is indexable only if 12+ real, original/licensed, apply-ready templates exist. If fewer than 12 apply-ready templates exist, `/templates` must be `noindex,follow` and omitted from sitemap.

Initial safe template set should be original CSS/Canvas configurations, not bitmap assets copied from platforms, anime, games, celebrities, or competitors.

```json
[
  {
    "id": "minimal-soft-ring",
    "name": "Minimal Soft Ring",
    "category": "minimal",
    "index_ready": true,
    "apply_ready": true,
    "preview_asset": null,
    "config": { "background": { "type": "solid", "value": "#F7F5F0" }, "border": { "color": "#111827", "width": 8, "opacity": 0.9 }, "shadow": { "color": "#111827", "blur": 24, "opacity": 0.12 } },
    "provenance": "original_css_canvas",
    "license": "site-owned original configuration",
    "source": [{ "label": "Compliance boundary", "note": "Self-made abstract/template configuration allowed." }],
    "confidence": "confirmed",
    "updated": "2026-06-03"
  },
  {
    "id": "neon-gaming-glow",
    "name": "Neon Gaming Glow",
    "category": "gaming",
    "index_ready": true,
    "apply_ready": true,
    "preview_asset": null,
    "config": { "background": { "type": "gradient", "value": "linear-gradient(135deg,#0F172A,#7C3AED,#22D3EE)" }, "border": { "color": "#22D3EE", "width": 10, "opacity": 0.95 }, "shadow": { "color": "#7C3AED", "blur": 36, "opacity": 0.35 } },
    "provenance": "original_css_canvas",
    "license": "site-owned original configuration",
    "source": [{ "label": "Compliance boundary", "note": "Generic gaming style; no platform logo, game IP, or character asset." }],
    "confidence": "confirmed",
    "updated": "2026-06-03"
  },
  {
    "id": "creator-clean-gradient",
    "name": "Creator Clean Gradient",
    "category": "creator",
    "index_ready": true,
    "apply_ready": true,
    "preview_asset": null,
    "config": { "background": { "type": "gradient", "value": "linear-gradient(135deg,#F97316,#FACC15)" }, "border": { "color": "#FFFFFF", "width": 12, "opacity": 1 }, "shadow": { "color": "#F97316", "blur": 30, "opacity": 0.22 } },
    "provenance": "original_css_canvas",
    "license": "site-owned original configuration",
    "source": [{ "label": "PRD template direction", "note": "Creator/social use case without platform brand assets." }],
    "confidence": "confirmed",
    "updated": "2026-06-03"
  },
  {
    "id": "professional-soft-blue",
    "name": "Professional Soft Blue",
    "category": "professional",
    "index_ready": true,
    "apply_ready": true,
    "preview_asset": null,
    "config": { "background": { "type": "gradient", "value": "linear-gradient(135deg,#E0F2FE,#DBEAFE)" }, "border": { "color": "#2563EB", "width": 6, "opacity": 0.7 }, "shadow": { "color": "#1D4ED8", "blur": 20, "opacity": 0.15 } },
    "provenance": "original_css_canvas",
    "license": "site-owned original configuration",
    "source": [{ "label": "PRD profile picture use case", "note": "Professional profile styling; not an AI headshot claim." }],
    "confidence": "confirmed",
    "updated": "2026-06-03"
  },
  {
    "id": "dark-circle-focus",
    "name": "Dark Circle Focus",
    "category": "dark",
    "index_ready": true,
    "apply_ready": true,
    "preview_asset": null,
    "config": { "background": { "type": "solid", "value": "#020617" }, "border": { "color": "#94A3B8", "width": 8, "opacity": 0.85 }, "shadow": { "color": "#000000", "blur": 40, "opacity": 0.4 } },
    "provenance": "original_css_canvas",
    "license": "site-owned original configuration",
    "source": [{ "label": "Compliance boundary", "note": "Original abstract style." }],
    "confidence": "confirmed",
    "updated": "2026-06-03"
  },
  {
    "id": "pastel-frame",
    "name": "Pastel Frame",
    "category": "pastel",
    "index_ready": true,
    "apply_ready": true,
    "preview_asset": null,
    "config": { "background": { "type": "gradient", "value": "linear-gradient(135deg,#FCE7F3,#EDE9FE,#DBEAFE)" }, "border": { "color": "#FFFFFF", "width": 14, "opacity": 0.95 }, "shadow": { "color": "#A78BFA", "blur": 28, "opacity": 0.2 } },
    "provenance": "original_css_canvas",
    "license": "site-owned original configuration",
    "source": [{ "label": "Compliance boundary", "note": "Generic pastel style; not child-directed." }],
    "confidence": "confirmed",
    "updated": "2026-06-03"
  },
  {
    "id": "discord-safe-margin",
    "name": "Discord Safe Margin",
    "category": "discord",
    "index_ready": true,
    "apply_ready": true,
    "preview_asset": null,
    "config": { "background": { "type": "gradient", "value": "linear-gradient(135deg,#1E293B,#334155)" }, "border": { "color": "#38BDF8", "width": 8, "opacity": 0.9 }, "shadow": { "color": "#38BDF8", "blur": 24, "opacity": 0.22 } },
    "provenance": "original_css_canvas",
    "license": "site-owned original configuration",
    "source": [{ "label": "Page matrix", "note": "Discord page requires safe-area/circle-preview guidance; no Discord logos or Blurple dependency." }],
    "confidence": "confirmed",
    "updated": "2026-06-03"
  },
  {
    "id": "transparent-clean-cut",
    "name": "Transparent Clean Cut",
    "category": "minimal",
    "index_ready": true,
    "apply_ready": true,
    "preview_asset": null,
    "config": { "background": { "type": "transparent", "value": "transparent" }, "border": { "color": "#FFFFFF", "width": 6, "opacity": 1 }, "shadow": { "color": "#000000", "blur": 18, "opacity": 0.18 } },
    "provenance": "original_css_canvas",
    "license": "site-owned original configuration",
    "source": [{ "label": "PRD P0 background options", "note": "Transparent if technically supported by browser export path." }],
    "confidence": "heuristic",
    "updated": "2026-06-03"
  },
  {
    "id": "sunset-ring",
    "name": "Sunset Ring",
    "category": "gradient",
    "index_ready": true,
    "apply_ready": true,
    "preview_asset": null,
    "config": { "background": { "type": "gradient", "value": "linear-gradient(135deg,#FB7185,#FDBA74,#FEF3C7)" }, "border": { "color": "#7C2D12", "width": 7, "opacity": 0.75 }, "shadow": { "color": "#FB7185", "blur": 32, "opacity": 0.24 } },
    "provenance": "original_css_canvas",
    "license": "site-owned original configuration",
    "source": [{ "label": "Compliance boundary", "note": "Original abstract gradient." }],
    "confidence": "confirmed",
    "updated": "2026-06-03"
  },
  {
    "id": "mono-outline",
    "name": "Mono Outline",
    "category": "minimal",
    "index_ready": true,
    "apply_ready": true,
    "preview_asset": null,
    "config": { "background": { "type": "solid", "value": "#FFFFFF" }, "border": { "color": "#000000", "width": 5, "opacity": 1 }, "shadow": { "color": "#000000", "blur": 0, "opacity": 0 } },
    "provenance": "original_css_canvas",
    "license": "site-owned original configuration",
    "source": [{ "label": "PRD P0 styling", "note": "Border/outline template." }],
    "confidence": "confirmed",
    "updated": "2026-06-03"
  },
  {
    "id": "soft-blur-backdrop",
    "name": "Soft Blur Backdrop",
    "category": "creator",
    "index_ready": true,
    "apply_ready": true,
    "preview_asset": null,
    "config": { "background": { "type": "blur", "value": "source_image_blur_24" }, "border": { "color": "#FFFFFF", "width": 10, "opacity": 0.9 }, "shadow": { "color": "#111827", "blur": 26, "opacity": 0.16 } },
    "provenance": "original_css_canvas",
    "license": "site-owned original configuration",
    "source": [{ "label": "PRD P0 background options", "note": "Blur background must be implemented locally from the uploaded image; do not upload the image." }],
    "confidence": "heuristic",
    "updated": "2026-06-03"
  },
  {
    "id": "text-ring-starter",
    "name": "Text Ring Starter",
    "category": "creator",
    "index_ready": true,
    "apply_ready": true,
    "preview_asset": null,
    "config": { "background": { "type": "solid", "value": "#F8FAFC" }, "border": { "color": "#0F172A", "width": 6, "opacity": 0.9 }, "shadow": { "color": "#0F172A", "blur": 16, "opacity": 0.12 }, "text_ring": { "enabled": true, "default_text": "YOUR NAME • CREATOR •" } },
    "provenance": "original_css_canvas",
    "license": "site-owned original configuration",
    "source": [{ "label": "PRD P0 styling", "note": "Circular/simple text is required P0 capability." }],
    "confidence": "confirmed",
    "updated": "2026-06-03"
  }
]
```

Templates index gate:

```ts
const templatesIndexReady = templates.filter(t => t.index_ready && t.apply_ready && t.provenance !== 'placeholder').length >= 12;
```

If implementation ships fewer than 12 apply-ready templates, set `/templates` robots to `noindex,follow` and omit it from sitemap even if the route exists.

## 4. Page config contract

```json
{
  "pages": [
    { "route": "/", "robots": "index,follow", "sitemap": true, "default_preset": "general", "required_schema": ["SoftwareApplication", "FAQPage"] },
    { "route": "/discord-pfp-maker", "robots": "index,follow", "sitemap": true, "default_preset": "discord", "required_schema": ["SoftwareApplication", "FAQPage"] },
    { "route": "/profile-picture-maker", "robots": "index,follow", "sitemap": true, "default_preset": "linkedin", "required_schema": ["SoftwareApplication", "FAQPage"] },
    { "route": "/ai-pfp-maker", "robots": "index,follow", "sitemap": true, "default_preset": "general", "feature_state": "ai_coming_next", "required_schema": ["SoftwareApplication", "FAQPage"] },
    { "route": "/templates", "robots": "conditional", "sitemap": "conditional", "condition": "12+ apply-ready original/licensed templates" },
    { "route": "/contact", "robots": "noindex,follow", "sitemap": false },
    { "route": "/privacy", "robots": "noindex,follow", "sitemap": false },
    { "route": "/terms", "robots": "noindex,follow", "sitemap": false }
  ],
  "global_disclaimer": "PFP Maker Online is an independent tool and is not affiliated with, endorsed by, sponsored by, or officially connected with Discord, Instagram, TikTok, YouTube, X, LinkedIn, Reddit, Twitch, or any other platform mentioned on this site. Platform names are used only to describe profile picture sizing and compatibility."
}
```

## 5. Cloudflare implementation checklist

### 5.1 Pages project

Expected Pages project name: `pfpmaker`.

Deploy target:

```bash
cd /root/projects/pfpmaker
npm run build
npx wrangler pages deploy <build-output-dir> --project-name pfpmaker --branch main
```

Actual build output depends on frontend stack:

- Astro/Vite: `dist`
- Next static export: `out`
- Next/OpenNext only if later API/server runtime is needed

P0 recommendation: Astro/Vite/static Next export is enough. Do not introduce OpenNext Worker runtime unless P1 APIs require it.

### 5.2 DNS / SSL / security

Before changing Cloudflare settings, follow Plan-before-Execute:

1. Query existing zone for `pfpmaker.online`.
2. Query current DNS records.
3. Query SSL, Always HTTPS, Bot Fight/Crawler Hints settings.
4. Only then create/update records or settings.

Target production state:

- `pfpmaker.online` proxied CNAME to Pages target or Pages custom domain binding.
- `www.pfpmaker.online` proxied CNAME to root or Pages target.
- SSL/TLS: Full (Strict).
- Always Use HTTPS: On.
- Browser Cache TTL: 4h.
- Static assets cached aggressively.
- No `/api/*` rate-limit rule needed for P0 if no API exists; add if optional waitlist/API is implemented.

### 5.3 Environment variables / secrets

P0 required secrets: none.

Do not create fake Stripe/Google/AI secrets.

Optional if implemented:

- `PUBLIC_SITE_URL=https://pfpmaker.online`
- `PUBLIC_ANALYTICS_PROVIDER=cloudflare|plausible|none`
- `WAITLIST_ENABLED=true` only if D1 waitlist exists
- `CONTACT_EMAIL=support@pfpmaker.online`

Future P1 only:

- AI provider keys
- Stripe keys
- JWT/OAuth secrets
- D1/R2 bindings

## 6. Privacy and security contract

P0 legal wording may say:

- “The core editor is designed to process basic edits in your browser where possible.”
- “We do not intentionally store your original uploaded image for the free browser editor.”
- “We do not use uploaded images to train AI models.”

P0 legal wording must not say unless QA proves it:

- “Your photo never leaves your device.”
- “100% private.”
- “No data collected.”

Engineering controls:

- Use `URL.createObjectURL` / FileReader locally.
- Strip EXIF from exported canvas output by default.
- Enforce max upload size in browser: recommended 10MB.
- Accept only `image/jpeg`, `image/png`, `image/webp`.
- Do not log file objects or file names to console in production.
- Do not persist images in localStorage/IndexedDB unless clearly disclosed; default no persistence.
- If Sentry/LogRocket/Clarity-like session replay is ever added, disable canvas/image capture or re-run compliance review.

## 7. P1 upgrade path

Only open backend implementation cards when the corresponding feature is real.

### 7.1 AI background removal / style tools

Required architecture:

- Worker API: `POST /api/ai/background-remove` or `POST /api/ai/style`
- Auth/rate limit: anonymous IP/session limit first, user credits later
- Storage: R2 temporary objects only if provider requires URL input
- Retention: delete temporary images within 24h, max 7 days
- D1: usage_records only if credits/quotas exist
- Queue: only if provider latency exceeds front-end timeout
- Legal: Privacy update with provider, purpose, retention, training policy

### 7.2 Paid Pro

Do not add checkout until:

- at least one paid feature exists
- AI/action cost is measured
- entitlement contract is written
- Stripe Tax readiness is complete

When enabled, use Stripe Checkout with:

```ts
automatic_tax: { enabled: true },
billing_address_collection: 'required',
tax_id_collection: { enabled: true }
```

Orders must distinguish subtotal, tax, and total.

## 8. Verification performed

Commands/tools run for this contract:

- `kanban_show(t_6515084d)` to read task, parent handoffs, and visibility target.
- `send_message(... telegram:-1003750190535:8008 ...)` START message sent successfully, message_id=8068.
- `search_files(/root/.hermes/reports/site-pfpmaker-20260603, *.md)` found upstream report files.
- `read_file` reviewed PRD, page matrix, pricing model, and compliance boundary.
- `terminal` checked filesystem state:
  - report directory exists
  - `/root/projects/pfpmaker` is currently missing
- `web_search` sampled current/public sizing references for Discord, Instagram, TikTok, and YouTube; remaining platform exact sizes are marked `needs_verification`.

No production Cloudflare settings were changed. No D1/R2/Pages project was created in this task because MVP architecture does not require backend resources and the expected project directory is not present.

## 9. Acceptance checklist

- [x] Read upstream PRD/page matrix/pricing/compliance/design handoffs.
- [x] Explicitly decided MVP can be static-first / browser-side.
- [x] Explicitly avoided unnecessary Workers/D1/R2/Stripe/OAuth/AI complexity for P0.
- [x] Defined optional D1 waitlist path if front-end includes a real waitlist form.
- [x] Defined analytics payload and forbidden image/privacy fields.
- [x] Defined platform presets contract with source/confidence/updated.
- [x] Marked unverified platform exact sizes as `needs_verification` instead of inventing official claims.
- [x] Defined 12-template apply-ready gate for `/templates` index/sitemap.
- [x] Defined safe original template seed configurations.
- [x] Defined Cloudflare deployment/security target and Plan-before-Execute reminder.
- [x] Defined P1 backend upgrade path for AI/R2/D1/Stripe.
- [x] Wrote deliverable to `/root/.hermes/reports/site-pfpmaker-20260603/backend-data-contract.md`.

## 10. Residual risks

1. `/root/projects/pfpmaker` does not exist at contract time. Frontend/dev must create or sync the repo before code-level implementation, git commit, push, or deploy can happen.
2. Exact platform image-size guidance for X, LinkedIn, Reddit, and Twitch remains `needs_verification`; do not claim official/recommended dimensions until checked against current platform docs or trusted current references.
3. `/templates` indexability depends on the front-end actually implementing 12 apply-ready original/licensed templates. The seed above is a contract; runtime QA must verify it is wired into the editor.
4. Local-first privacy copy depends on real network behavior. QA must test upload/edit/download with browser Network tab and confirm images are not sent to server/third-party endpoints.
5. If any future AI/server-side image processing is added, compliance and Privacy must be re-run before launch.
6. Domain remains close to pfpmaker.com; backend/data contract cannot mitigate visual/copy confusion alone.

## 11. Next inputs

To frontend/dev:

- Create/sync `/root/projects/pfpmaker`.
- Implement static data files from this contract.
- Keep editor image processing local; no image upload API in P0.
- Wire page config to robots/sitemap/canonical.
- Set `/templates` noindex unless 12 apply-ready templates are real.
- Verify browser Network tab before launch.

To QA:

- Upload a local test image and confirm no image network upload.
- Check analytics events for forbidden fields.
- Check `/ai-pfp-maker` feature-state copy.
- Check platform no-affiliation disclaimer.
- Check templates provenance and no IP/logo assets.

To infra/ops:

- Before DNS/SSL/security changes, query current Cloudflare zone/records/settings first.
- Configure Email Routing or equivalent aliases for `support@pfpmaker.online`, `privacy@pfpmaker.online`, and `legal@pfpmaker.online` before launch if used in legal pages.
