# 05B Backend / API / Infra Contract — aieditorrsp.net

- task_id: t_64e4e44b
- tenant: site-aieditorrsp-20260528
- project_slug: aieditorrsp
- fixed_domain: aieditorrsp.net
- product_name: AI Editor RSP
- primary_keyword: AI image editor with prompt
- backend_owner: 墨枢
- prepared_at_utc: 2026-05-28T15:10:02Z
- verdict: BACKEND_CONTRACT_GO_WITH_INFRA_READINESS_GAPS

## 1. 结论

后端必须按 Cloudflare Workers / OpenNext-first 交付，不能做纯静态站。

P0 后端范围：
- server-side image edit API proxy: `/api/generate-image`
- anonymous free quota: 2 generations/day, keyed by IP hash + anonymous session
- upload validation: JPG/PNG/WebP, MIME + magic bytes + size limit
- unsafe prompt blocking: celebrity/public figure, deepfake, explicit, document/watermark/signature/fraud categories
- D1: users/sessions/credits/generations/prompt templates/payments/abuse logs
- KV: short-window rate limit and request dedupe
- R2: optional generated output storage; default anonymous retention 24h
- Stripe Checkout/webhook only if paid ships in P0; if enabled, automatic tax and subtotal/tax/total separation are mandatory
- legal routes and redirects are frontend routes, but backend contract must expose provider/retention/payment facts for the legal pages

Do not expose AI provider keys in the browser bundle. Do not deduct credits before provider success.

## 2. Inputs read

- `/root/.hermes/reports/site-aieditorrsp-20260528/input-brief.md`
- `/root/.hermes/reports/site-aieditorrsp-20260528/03-prd-v1.md`
- `/root/.hermes/reports/site-aieditorrsp-20260528/02a-pricing.md`
- `/root/.hermes/reports/site-aieditorrsp-20260528/02b-compliance.md`
- `/root/.hermes/reports/site-aieditorrsp-20260528/02c-seo-copy.md`
- `/root/.hermes/reports/site-aieditorrsp-20260528/design/HANDOFF.md`
- parent handoffs: `t_515e68e7`, `t_cbe1516a`

## 3. Runtime architecture

Recommended production shape:

```text
Next.js app
  ↓ OpenNext Cloudflare / Workers Static Assets
Cloudflare Worker runtime
  ├─ ASSETS binding: static app assets
  ├─ DB binding: D1 database `aieditorrsp-db`
  ├─ USER_UPLOADS / RESULTS binding: R2 bucket `aieditorrsp-assets`
  ├─ RATE_LIMIT binding: KV namespace `aieditorrsp-rate-limit`
  ├─ `/api/generate-image`: validate → quota → safety → provider → record → return
  ├─ `/api/credits`: quota/credits state
  ├─ `/api/prompt-templates`: structured template data if dynamic
  ├─ `/api/checkout`: Stripe Checkout if paid enabled
  └─ `/api/stripe/webhook`: raw-body webhook verification
```

Static-only is rejected because the PRD requires real image editing, server-side provider calls, quota, safety controls, and paid credits.

Pages Functions fallback is acceptable only if the frontend build chooses static Pages, but the same API/data contract applies.

## 4. Cloudflare resource contract

### 4.1 Bindings

Use these names so frontend/backend code has stable contracts:

```toml
name = "aieditorrsp"
compatibility_date = "2026-05-28"
compatibility_flags = ["nodejs_compat"]

[[d1_databases]]
binding = "DB"
database_name = "aieditorrsp-db"
database_id = "<create-and-fill>"

[[r2_buckets]]
binding = "RESULTS"
bucket_name = "aieditorrsp-assets"

[[kv_namespaces]]
binding = "RATE_LIMIT"
id = "<create-and-fill>"

[vars]
APP_ORIGIN = "https://aieditorrsp.net"
SITE_NAME = "AI Editor RSP"
FREE_DAILY_GENERATIONS = "2"
ANON_RESULT_TTL_HOURS = "24"
```

If using OpenNext, merge these bindings into the generated Worker config rather than overwriting frontend settings.

### 4.2 Required secrets

Do not commit values. Report only present/missing in future implementation tasks.

```text
AI provider:
- FAL_KEY or REPLICATE_API_TOKEN or OPENAI_API_KEY or GEMINI_API_KEY
- AI_PROVIDER_NAME
- AI_PROVIDER_MODEL

Auth/session:
- SESSION_SECRET
- JWT_SECRET if JWT is separate
- GOOGLE_CLIENT_ID if login ships
- GOOGLE_CLIENT_SECRET if login ships

Stripe if paid ships:
- STRIPE_SECRET_KEY
- STRIPE_WEBHOOK_SECRET
- STRIPE_PRICE_ID_PRO_MONTHLY
- STRIPE_PRICE_ID_PRO_ANNUAL
- STRIPE_PRICE_ID_CREDIT_PACK

Security/ops:
- TURNSTILE_SECRET_KEY if anonymous abuse appears
- SENTRY_DSN only if error monitoring ships
```

Secrets status in this contract run: not created/verified because no project repo or Worker/Pages project exists yet.

## 5. D1 schema contract

Create `migrations/0001_init.sql` with this baseline. It is intentionally explicit so quota/payment/audit can be verified without reading raw logs.

```sql
PRAGMA foreign_keys = ON;

CREATE TABLE IF NOT EXISTS users (
  id TEXT PRIMARY KEY,
  email TEXT UNIQUE,
  name TEXT,
  avatar_url TEXT,
  google_id TEXT UNIQUE,
  role TEXT NOT NULL DEFAULT 'user' CHECK (role IN ('user','admin')),
  plan TEXT NOT NULL DEFAULT 'free' CHECK (plan IN ('free','pro','admin')),
  status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','suspended','deleted')),
  stripe_customer_id TEXT UNIQUE,
  created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_stripe_customer_id ON users(stripe_customer_id);

CREATE TABLE IF NOT EXISTS anonymous_sessions (
  id TEXT PRIMARY KEY,
  session_hash TEXT NOT NULL UNIQUE,
  ip_hash TEXT NOT NULL,
  user_agent_hash TEXT,
  country TEXT,
  first_seen_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
  last_seen_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
  generation_count INTEGER NOT NULL DEFAULT 0
);

CREATE INDEX IF NOT EXISTS idx_anon_ip_hash ON anonymous_sessions(ip_hash);

CREATE TABLE IF NOT EXISTS credit_accounts (
  id TEXT PRIMARY KEY,
  user_id TEXT,
  anonymous_session_id TEXT,
  plan TEXT NOT NULL DEFAULT 'free',
  monthly_allowance INTEGER NOT NULL DEFAULT 0,
  monthly_used INTEGER NOT NULL DEFAULT 0,
  paid_credit_balance INTEGER NOT NULL DEFAULT 0,
  free_daily_limit INTEGER NOT NULL DEFAULT 2,
  free_daily_used INTEGER NOT NULL DEFAULT 0,
  free_daily_date TEXT,
  period_start TEXT,
  period_end TEXT,
  created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES users(id),
  FOREIGN KEY (anonymous_session_id) REFERENCES anonymous_sessions(id)
);

CREATE UNIQUE INDEX IF NOT EXISTS idx_credit_accounts_user ON credit_accounts(user_id) WHERE user_id IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS idx_credit_accounts_anon ON credit_accounts(anonymous_session_id) WHERE anonymous_session_id IS NOT NULL;

CREATE TABLE IF NOT EXISTS credit_transactions (
  id TEXT PRIMARY KEY,
  credit_account_id TEXT NOT NULL,
  user_id TEXT,
  anonymous_session_id TEXT,
  generation_id TEXT,
  order_id TEXT,
  type TEXT NOT NULL CHECK (type IN ('grant','consume','refund','expire','admin_adjust','provider_refund')),
  amount INTEGER NOT NULL,
  balance_after INTEGER,
  reason TEXT,
  metadata_json TEXT,
  created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (credit_account_id) REFERENCES credit_accounts(id),
  FOREIGN KEY (user_id) REFERENCES users(id),
  FOREIGN KEY (anonymous_session_id) REFERENCES anonymous_sessions(id)
);

CREATE INDEX IF NOT EXISTS idx_credit_transactions_account_time ON credit_transactions(credit_account_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_credit_transactions_type_time ON credit_transactions(type, created_at DESC);

CREATE TABLE IF NOT EXISTS prompt_templates (
  id TEXT PRIMARY KEY,
  slug TEXT NOT NULL UNIQUE,
  name TEXT NOT NULL,
  category TEXT NOT NULL,
  best_for TEXT,
  prompt TEXT NOT NULL,
  negative_prompt TEXT,
  preserves TEXT,
  example_use_case TEXT,
  safety_note TEXT NOT NULL,
  indexable INTEGER NOT NULL DEFAULT 0 CHECK (indexable IN (0,1)),
  status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','draft','archived')),
  created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_prompt_templates_status_category ON prompt_templates(status, category);

CREATE TABLE IF NOT EXISTS generations (
  id TEXT PRIMARY KEY,
  user_id TEXT,
  anonymous_session_id TEXT,
  prompt_template_id TEXT,
  status TEXT NOT NULL CHECK (status IN ('queued','processing','succeeded','failed','blocked')),
  blocked_category TEXT,
  input_mime TEXT,
  input_size_bytes INTEGER,
  input_r2_key TEXT,
  output_r2_key TEXT,
  provider TEXT NOT NULL,
  provider_model TEXT NOT NULL,
  provider_request_id TEXT,
  provider_cost_estimate_cents INTEGER,
  credit_cost INTEGER NOT NULL DEFAULT 1,
  error_code TEXT,
  error_message TEXT,
  result_expires_at TEXT,
  created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
  completed_at TEXT,
  FOREIGN KEY (user_id) REFERENCES users(id),
  FOREIGN KEY (anonymous_session_id) REFERENCES anonymous_sessions(id),
  FOREIGN KEY (prompt_template_id) REFERENCES prompt_templates(id)
);

CREATE INDEX IF NOT EXISTS idx_generations_user_time ON generations(user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_generations_anon_time ON generations(anonymous_session_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_generations_status_time ON generations(status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_generations_blocked_category ON generations(blocked_category, created_at DESC);

CREATE TABLE IF NOT EXISTS abuse_events (
  id TEXT PRIMARY KEY,
  user_id TEXT,
  anonymous_session_id TEXT,
  ip_hash TEXT,
  event_type TEXT NOT NULL,
  category TEXT,
  request_path TEXT,
  metadata_json TEXT,
  created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES users(id),
  FOREIGN KEY (anonymous_session_id) REFERENCES anonymous_sessions(id)
);

CREATE INDEX IF NOT EXISTS idx_abuse_events_ip_time ON abuse_events(ip_hash, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_abuse_events_category_time ON abuse_events(category, created_at DESC);

CREATE TABLE IF NOT EXISTS orders (
  id TEXT PRIMARY KEY,
  user_id TEXT NOT NULL,
  provider TEXT NOT NULL DEFAULT 'stripe',
  stripe_checkout_session_id TEXT UNIQUE,
  stripe_subscription_id TEXT,
  stripe_customer_id TEXT,
  plan TEXT NOT NULL CHECK (plan IN ('pro_monthly','pro_annual','credit_pack')),
  credits_granted INTEGER NOT NULL DEFAULT 0,
  subtotal_amount INTEGER NOT NULL,
  tax_amount INTEGER NOT NULL DEFAULT 0,
  total_amount INTEGER NOT NULL,
  currency TEXT NOT NULL DEFAULT 'usd',
  status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','paid','refunded','partial_refund','failed','canceled')),
  tax_status TEXT,
  raw_event_id TEXT,
  created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
  paid_at TEXT,
  updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES users(id)
);

CREATE INDEX IF NOT EXISTS idx_orders_user_time ON orders(user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_orders_subscription ON orders(stripe_subscription_id);

CREATE TABLE IF NOT EXISTS webhook_events (
  id TEXT PRIMARY KEY,
  provider TEXT NOT NULL,
  event_id TEXT NOT NULL,
  event_type TEXT NOT NULL,
  processed_at TEXT,
  status TEXT NOT NULL DEFAULT 'received' CHECK (status IN ('received','processed','ignored','failed')),
  error_message TEXT,
  created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
  UNIQUE(provider, event_id)
);
```

Migration rule: verify `wrangler.toml/jsonc` binding database_name/database_id matches the migration target before `--remote` execution.

## 6. API schema

All API responses use JSON unless returning binary/image download. Error shape:

```json
{
  "ok": false,
  "error": {
    "code": "QUOTA_EXCEEDED",
    "message": "You used today’s 2 free edits.",
    "retry_after_seconds": 3600
  }
}
```

### 6.1 Health

`GET /api/health`

Response 200:

```json
{
  "ok": true,
  "service": "aieditorrsp",
  "runtime": "cloudflare-workers",
  "version": "<commit-sha>",
  "time": "2026-05-28T15:10:02Z"
}
```

### 6.2 Credits

`GET /api/credits`

Auth: optional. Anonymous session cookie allowed.

Response 200:

```json
{
  "ok": true,
  "plan": "free",
  "free_daily_limit": 2,
  "free_daily_used": 0,
  "free_daily_remaining": 2,
  "monthly_allowance": 0,
  "monthly_used": 0,
  "paid_credit_balance": 0,
  "next_reset_at": "2026-05-29T00:00:00Z",
  "upgrade_available": true
}
```

### 6.3 Prompt templates

`GET /api/prompt-templates?category=portrait&limit=20`

Response 200:

```json
{
  "ok": true,
  "templates": [
    {
      "id": "tpl_cinematic_portrait",
      "slug": "cinematic-portrait",
      "name": "Cinematic Portrait",
      "category": "Portrait",
      "best_for": "creator profile photos",
      "prompt": "Turn this portrait into...",
      "negative_prompt": "Do not change facial structure...",
      "preserves": ["face identity", "natural skin texture"],
      "safety_note": "Use only with images you own or have permission to edit."
    }
  ]
}
```

Frontend may also ship templates as static structured data, but the field names above are canonical.

### 6.4 Generate image

`POST /api/generate-image`

Auth: optional P0. Anonymous session accepted until abuse requires Turnstile/login.

Request: `multipart/form-data`

Fields:

```text
image: File, required, JPG/PNG/WebP
prompt: string, required, 20..4000 chars
template_id: string, optional
negative_prompt: string, optional
output_size: "1024" | "1536", default "1024"
intent: "portrait" | "product" | "background" | "social" | "headshot" | "other"
turnstile_token: string, optional when challenged
```

Processing order:
1. Resolve user or anonymous session.
2. Enforce global rate limit from KV.
3. Validate content length before reading full body.
4. Validate MIME and magic bytes; reject unsupported file types.
5. Strip/ignore EXIF; do not use GPS metadata.
6. Run prompt guardrail. If blocked, create `generations.status='blocked'`, log `abuse_events`, do not consume credits.
7. Check quota/credits. If insufficient, return 402 and do not call provider.
8. Call AI provider server-side with secret from Worker env.
9. On provider success: store output if R2 retention is enabled, insert generation, consume exactly 1 credit, return result.
10. On provider failure: insert failed generation, do not consume credits.

Success response 200:

```json
{
  "ok": true,
  "generation_id": "gen_...",
  "status": "succeeded",
  "result": {
    "image_url": "https://aieditorrsp.net/api/results/gen_...",
    "expires_at": "2026-05-29T15:10:02Z",
    "download_url": "https://aieditorrsp.net/api/results/gen_...?download=1"
  },
  "credits": {
    "consumed": 1,
    "remaining_today": 1,
    "paid_credit_balance": 0
  },
  "provider": {
    "name": "fal.ai",
    "model": "fal-ai/flux-2-pro/edit"
  },
  "disclaimer": "AI results may vary. You are responsible for input rights and output use."
}
```

Typed errors:

| HTTP | code | Meaning |
|---:|---|---|
| 400 | INVALID_IMAGE | unsupported MIME, too large, corrupt image |
| 400 | INVALID_PROMPT | prompt too short/long or missing |
| 403 | UNSAFE_PROMPT_BLOCKED | celebrity/IP/explicit/document/watermark/deepfake/fraud request |
| 402 | QUOTA_EXCEEDED | free daily or paid credits exhausted |
| 429 | RATE_LIMITED | abuse/rate limit exceeded |
| 503 | PROVIDER_UNAVAILABLE | provider failed before success; no credit consumed |
| 500 | INTERNAL_ERROR | unexpected backend failure |

### 6.5 Result fetch/download

`GET /api/results/:generation_id`

Auth: anonymous signed access token or owner account if result is private.

Response:
- 200 image stream if authorized and not expired
- 404 if missing/expired
- 403 if not owner/token mismatch

Do not expose raw private R2 object URLs.

### 6.6 Checkout if paid ships

`POST /api/checkout`

Auth: recommended if subscription ships; anonymous credit-pack checkout can be allowed only with email capture.

Request:

```json
{
  "plan": "pro_monthly | pro_annual | credit_pack",
  "return_path": "/pricing"
}
```

Stripe Checkout creation requirements:

```ts
automatic_tax: { enabled: true },
billing_address_collection: 'required',
tax_id_collection: { enabled: true },
allow_promotion_codes: true,
mode: plan === 'credit_pack' ? 'payment' : 'subscription',
metadata: {
  project_slug: 'aieditorrsp',
  user_id,
  plan
}
```

Response 200:

```json
{
  "ok": true,
  "checkout_url": "https://checkout.stripe.com/..."
}
```

### 6.7 Stripe webhook if paid ships

`POST /api/stripe/webhook`

Rules:
- use `request.text()` raw body
- verify `stripe-signature` with `STRIPE_WEBHOOK_SECRET`
- idempotency via `webhook_events(provider,event_id)`
- process only verified events
- store subtotal/tax/total separately

Events:
- `checkout.session.completed`: mark order paid, grant credits / activate plan
- `customer.subscription.updated`: update plan/period/allowance
- `customer.subscription.deleted`: downgrade at period end or immediate depending Stripe state
- `invoice.paid`: renew monthly credits if subscription mode uses invoices
- `charge.refunded`: update order/refund state and credit adjustments if required

## 7. Quota and credit state machine

P0 free quota:
- Free: 2 generations/day.
- Anonymous: keyed by `session_hash + ip_hash + date`.
- Logged-in Free: keyed by `user_id + date`.
- Abuse fallback: reduce to 1/day or require Turnstile/login.

Credit consumption rule:
- Unsafe prompt: no credit consumed.
- Provider validation error before generation: no credit consumed.
- Provider outage/failure: no credit consumed.
- Successful provider output: consume 1 credit.
- If output is generated but R2 save fails, return provider URL if safe/short-lived; log storage failure; do not double-charge.

Priority of balances:
1. free_daily_remaining for Free anonymous/logged-in
2. monthly Pro allowance
3. paid credit pack balance
4. admin/user override if needed

## 8. Safety guardrail contract

Minimum server-side block categories:

```text
public_figure_impersonation
celebrity_or_ip
political_impersonation
explicit_or_nsfw
minor_or_child_safety
deepfake_or_face_swap
document_or_id_edit
watermark_or_signature_removal
fraud_or_deceptive_ad
rights_or_brand_infringement
rate_limit_abuse
```

Minimum keyword first pass includes:

```text
celebrity, famous actor, politician, president, prime minister, public figure,
deepfake, face swap, nude, naked, explicit, nsfw, minor, child,
passport, id card, driver license, invoice, receipt, watermark remove,
forged, fake document, signature
```

Do not rely only on keywords. Provider safety filters still apply. Paid users cannot bypass unsafe blocks.

## 9. Storage and retention

Default P0:
- Do not permanently save anonymous uploaded originals.
- Anonymous generated outputs expire after 24h.
- Logged-in generation history, if shipped, max 30d unless user explicitly saves.
- Store only metadata needed for billing, reliability, and abuse prevention.
- Do not log raw image content or full sensitive prompt text in generic logs.
- R2 object key pattern:
  - `uploads/anon/YYYY/MM/DD/<generation_id>.<ext>` only if provider requires temporary staging
  - `results/anon/YYYY/MM/DD/<generation_id>.webp`
  - `results/users/<user_id>/YYYY/MM/DD/<generation_id>.webp`

A cleanup job is required if R2 persistence is enabled:
- Cron Trigger or scheduled worker daily.
- Delete expired anonymous outputs.
- Mark expired rows or clear `output_r2_key` if deletion succeeds.

## 10. Frontend integration contract

Required frontend-visible states from backend:

```text
idle
uploading
upload_validation_error
uploaded_preview
prompt_selected
builder_active
generating
generation_success
provider_fail
free_limit_reached
upgrade_prompt
unsafe_prompt_blocked
rate_limited
```

The backend must return typed error codes so 墨界 can map to design states, not parse free text.

Frontend must never call provider APIs directly and must not include provider keys in client code.

## 11. Legal/support facts backend must expose to pages

Launch blockers unless resolved before production:
- contact email: recommended `support@aieditorrsp.net` after MX setup
- final AI provider name and provider retention/training terms
- final analytics stack and whether consent banner is implemented
- final refund window: recommended 7 days for unused or mostly unused paid credits; consumed credits generally not refunded
- paid checkout enabled/disabled state

Legal routes required by frontend:
- `/privacy`
- `/terms`
- `/cookie-policy`
- `/refund`
- `/contact`
- `/privacy-policy` → `/privacy` 308
- `/terms-of-service` → `/terms` 308

## 12. Cloudflare zone / DNS / security readiness

Verified at 2026-05-28T15:10:02Z:

```json
{
  "zone_id": "9313bcd5de4003b7cf1eefd3e844cb20",
  "zone_name": "aieditorrsp.net",
  "zone_status": "active",
  "zone_type": "full",
  "assigned_nameservers": ["coraline.ns.cloudflare.com", "nico.ns.cloudflare.com"],
  "dig_ns_observed": ["coraline.ns.cloudflare.com", "nico.ns.cloudflare.com"],
  "dns_records": [],
  "dig_a_observed": false,
  "www_cname_observed": false,
  "https_code": "000",
  "ssl_setting": "full",
  "always_use_https": "on",
  "browser_cache_ttl": 14400,
  "rate_limit_ruleset": "missing",
  "active_page_rules": 0,
  "bot_fight_mode": "setting unavailable for this zone/API plan or endpoint",
  "crawler_hints": "setting unavailable for this zone/API plan or endpoint"
}
```

Readiness interpretation:
- Nameserver delegation is complete and Cloudflare zone is active.
- No app DNS records exist yet.
- Production HTTPS is not live.
- SSL mode is `full`, not required `strict`; do not switch to strict until the production Worker/Pages route is bound and HTTPS has been checked, because SSL mode changes can break origin HTTPS if the origin path changes.
- Always Use HTTPS and 4h browser cache TTL are already set.
- API route rate limit is missing.
- Static asset cache rule is missing.
- Crawler Hints/Bot Fight settings could not be read through this account/API endpoint; launch worker should verify in dashboard or use the correct ruleset/settings API if available.

No Cloudflare mutation was executed in this contract task.

## 13. Infra execution plan for build task

After 墨界 creates repo/Pages/Worker project and the deployment target is known:

1. Create resources if missing:
   - `npx wrangler d1 create aieditorrsp-db`
   - `npx wrangler r2 bucket create aieditorrsp-assets`
   - `npx wrangler kv namespace create RATE_LIMIT`
2. Merge bindings into `wrangler.toml/jsonc`; do not overwrite frontend config.
3. Add `migrations/0001_init.sql` from this contract and run:
   - local syntax: `sqlite3 :memory: < migrations/0001_init.sql`
   - remote after binding check: `npx wrangler d1 execute aieditorrsp-db --remote --file=migrations/0001_init.sql`
4. Set secrets with `wrangler secret put` or Pages secret commands for the actual project.
5. Configure DNS only after deployment target exists:
   - apex `aieditorrsp.net` to Worker route / Pages custom domain
   - `www.aieditorrsp.net` redirect or CNAME to apex
6. CF security after route is live:
   - set SSL/TLS to Full (Strict) only after HTTPS route works
   - keep Always Use HTTPS on
   - enable Crawler Hints if available
   - add `/api/*` rate limit, suggested 100 req/min/IP with stricter `/api/generate-image` threshold
   - add static asset cache rule for `_next/static/*` or Worker static assets
7. Deploy from same commit that is pushed.
8. Verify custom domain, API health, quota behavior, unsafe block, provider failure no-charge, and Stripe checkout/webhook if enabled.

## 14. Acceptance checklist for implementation

Backend/API:
- `GET /api/health` returns 200.
- `GET /api/credits` returns anonymous quota without login.
- `POST /api/generate-image` accepts valid JPG/PNG/WebP and returns typed success.
- Unsupported file type returns `INVALID_IMAGE`.
- Unsafe prompt returns `UNSAFE_PROMPT_BLOCKED` and consumes 0 credits.
- Free quota reaches `QUOTA_EXCEEDED` after 2 successful generations/day.
- Provider failure returns `PROVIDER_UNAVAILABLE` and consumes 0 credits.
- No provider API key appears in client bundle.
- D1 has required tables and indexes.
- R2 results, if stored, are private and served through `/api/results/:id`.

Payments if enabled:
- Checkout has `automatic_tax.enabled=true`.
- Checkout has `billing_address_collection='required'`.
- Checkout has `tax_id_collection.enabled=true`.
- Webhook verifies raw body signature.
- Order stores subtotal/tax/total separately.
- Subscription cancellation downgrades plan correctly.

Infra:
- D1/R2/KV bindings in config match actual resource IDs.
- DNS records exist for apex and www.
- HTTPS returns 200 on production domain.
- SSL is Full (Strict) after route is live.
- API rate limit exists.
- sitemap/robots/index policy are consistent with PRD.
- Git status clean after commit; pushed commit equals deployment source commit.

## 15. Residual risks

- Final image provider/model is not locked; quota/pricing must be rechecked if unit cost exceeds $0.06/generation.
- Anonymous free generation abuse is material; Turnstile/login fallback should be ready.
- `aieditorrsp.net` readability remains weak; backend cannot mitigate except preserving route/metadata contract.
- Legal pages still need final contact email, provider retention/training terms, analytics/cookie behavior, and refund window.
- Cloudflare app DNS/security is not fully configured because no repo/project/deployment target exists in this task.
- Stripe product/price IDs are not available; paid checkout should be feature-flagged off until secrets and tax settings are verified.

## 16. Next inputs

For 墨界 / build task:
- Use this file as backend contract.
- Create Next.js/OpenNext Cloudflare project/repo for `aieditorrsp`.
- Implement API routes and state mapping exactly as above.
- Seed at least 20 safe structured prompt templates.
- Return typed errors for all design states.

For infra/launch task:
- Actual Worker/Pages project name and deployment URL.
- D1 database ID, R2 bucket creation result, KV namespace ID.
- Selected AI provider/model and secret name.
- Stripe product/price IDs if paid ships.
- Contact email and MX/email routing state.
- Approval to execute DNS/security mutations after deployment target exists.

## 17. Verification performed

Commands/checks completed:
- Loaded `kanban-worker`, `backend-site-build`, and `cloudflare-pages-functions-d1-r2-backend` guidance.
- Read PRD, pricing, compliance, SEO copy, and design handoff artifacts.
- Verified `/root/projects/aieditorrsp` does not exist yet, so this task should produce a contract rather than patch code.
- `dig +short NS aieditorrsp.net` returns Cloudflare NS: `coraline.ns.cloudflare.com`, `nico.ns.cloudflare.com`.
- Cloudflare API read confirms zone `aieditorrsp.net` is `active` and has zero DNS records.
- `dig +short A aieditorrsp.net` returns empty.
- `dig +short CNAME www.aieditorrsp.net` returns empty.
- `curl https://aieditorrsp.net` returns HTTP `000`.
- Wrangler read checks show no existing D1 database or R2 bucket named for `aieditorrsp`.
- Cloudflare settings read: SSL `full`, Always HTTPS `on`, browser cache TTL `14400`, no rate limit entrypoint, no active page rules.

Artifact path:
- `/root/.hermes/reports/site-aieditorrsp-20260528/05b-backend.md`
