{
  "version": 1,
  "owner": "P2HS Core — immutable. Style the UI; do not change these URLs or invent a second backend.",
  "client": {
    "nuxt_app": "composables/useP2hs.ts",
    "react_app": "src/lib/p2hs-app.js",
    "html": "js/p2hs-app.js"
  },
  "recipes": {
    "submit_form": {
      "use": "submitGeneratedForm({ formKey, values, challenge, honeypot })",
      "route": "POST /api/forms/:formKey/submissions",
      "notes": [
        "Canonical P2HS Forms for new contact/quote/intake. Flat values only. Do not send project_id, actions, or definition.",
        "Success is ok+accepted. Do not require submission_id. Do not call CRM/email/automation from the client.",
        "Owner notify and confirmation are Forms post-submit actions that reference catalog template keys. Do not copy template HTML into the app.",
        "Do not also call submitContact / POST /api/contact for the same new form."
      ]
    },
    "submit_canonical_form": {
      "use": "submitGeneratedForm({ formKey, values, challenge, honeypot })",
      "route": "POST /api/forms/:formKey/submissions",
      "notes": [
        "Alias of submit_form. Kept so older prompts that named this recipe still resolve."
      ]
    },
    "submit_form_legacy": {
      "use": "submitContact({ name, email, phone, message, website: '', turnstileToken })",
      "route": "POST /api/contact",
      "status": "compatibility_only",
      "notes": [
        "Already-generated apps only. Not for new project generation.",
        "website is a hidden honeypot. Do not POST to Resend, Nodemailer, or a homemade functions/api/contact.js.",
        "Owner notification on this leftover path uses Cloud render of contact_capture.admin_notify. Do not add a second mailer."
      ]
    },
    "send_email": {
      "use": "sendEmail({ to, subject, text, html? }, idempotencyKey)",
      "route": "POST /api/jobs/email.send",
      "auth": "staff Bearer",
      "notes": [
        "Always send Idempotency-Key. Poll job.poll_url if status is not terminal.",
        "New Forms contact/quote owner mail is handled by post-submit actions, not this helper. Do not duplicate it here.",
        "Leftover POST /api/contact apps already notify via that route — do not add a second mailer there either."
      ]
    },
    "upload_application_file": {
      "use": "uploadApplicationFile(file)",
      "route": "POST /api/files",
      "notes": [
        "Provider-neutral application file. Returns file_id. Do not send connection_id, object_key, storage_authority, or provider.",
        "Project configuration chooses [P] or [C] backing. Do not call storage.presign_upload or storage.file.put from ordinary file widgets.",
        "Attach with attachFile(file_id, subject_entity_key, subject_record_id) after the subject record exists."
      ]
    },
    "pull_records": {
      "use": "listRecords(entityKey) / getRecord(entityKey, id) / createRecord(entityKey, data) / patchRecord(entityKey, id, data)",
      "route": "/api/records/:entity",
      "auth": "staff Bearer unless the entity is public-read (owner_managed list)",
      "notes": [
        "Reuse database.entities keys. Never invent SQLite, KV, JSON files, or useStorage as source of truth.",
        "Contact inbox entity key is exactly contact_submission."
      ]
    },
    "contact_inbox": {
      "use": "listInbox()",
      "route": "GET /api/contact/inbox",
      "auth": "staff Bearer"
    },
    "magic_link": {
      "request": "requestMagicLink(email, { adminOnly: true }) from the staff URL",
      "request_route": "POST /api/auth/magic-link/request",
      "consume_route": "GET /api/auth/magic-link/consume?token=…&next=…",
      "notes": [
        "The emailed URL is always the consume route. Never create /admin/magic or put that path in the email.",
        "Consume stores access_token in localStorage then redirects to next (/admin, /be-admin, …).",
        "Staff fetches send Authorization: Bearer <access_token>."
      ]
    },
    "staff_dashboard": {
      "path": "/admin (or the path the user named, e.g. /be-admin)",
      "gate": "me() then require role admin or super_admin",
      "login": "If me().user is null, show email field and requestMagicLink",
      "data": "listInbox() and/or listRecords(entityKey)",
      "notes": [
        "One auth system. No local password table, user-db.ts, or hardcoded admin password.",
        "Invite additional staff with the staff-admin-console pack, not a homemade users table."
      ]
    }
  },
  "forbidden": [
    "p2hsClient / Cloud gateway calls from the browser",
    "/admin/magic landing pages",
    "Resend, Nodemailer, SMTP, or mailto-only as the contact backend",
    "local JSON/SQLite/KV as the records or users database",
    "Turnstile keys in required-user-inputs.json"
  ]
}
