{
  "name": "Lily",
  "id": "lily",
  "version": "0.1.0",
  "maturity": "thin-real-worker-d1-ui-port; ticket-desk-contract-draft",
  "category": "support",
  "spine": {
    "id": "ticket-desk",
    "record": "ticket: a thread with classification fields, a number and an assignee"
  },
  "replaces": [
    {
      "name": "Zendesk",
      "edition": null
    }
  ],
  "description": "A self-hostable ticket desk with numbered cases, shared views, Help Centre intake, configurable channels, service targets, customer records, and operational analytics.",
  "scope": "One business, one shared support workspace, with role and group access controls.",
  "capabilities": [
    {
      "id": "workspace",
      "description": "A public support front door and role-aware authenticated operator workspace.",
      "clauses": [
        "HOME-001",
        "APP-001",
        "ACCESS-001",
        "ACCESS-002",
        "ACCESS-003",
        "ACCESS-004"
      ]
    },
    {
      "id": "ticket-desk",
      "description": "Numbered case records, hidden default conversations, timeline work, saved views, forms, assignment, merge, and mobile agent access.",
      "clauses": [
        "TKT-001",
        "TKT-002",
        "TKT-003",
        "TKT-004",
        "TKT-005",
        "TKT-006",
        "TKT-007",
        "TKT-008",
        "TKT-009",
        "TKT-010",
        "TKT-011",
        "TKT-012",
        "TKT-013",
        "TKT-014",
        "TKT-015",
        "TKT-016",
        "TKT-017",
        "TKT-018",
        "CASE-001",
        "CASE-002",
        "CASE-003",
        "CASE-004",
        "CASE-005",
        "CASE-006",
        "CASE-007",
        "CASE-008",
        "CASE-009",
        "CASE-010"
      ]
    },
    {
      "id": "email-and-channels",
      "description": "Durable email intake and delivery plus configurable widget, bot, proactive, voice, and side-conversation channels.",
      "clauses": [
        "MAIL-001",
        "MAIL-002",
        "MAIL-003",
        "MAIL-004",
        "MAIL-005",
        "MAIL-006",
        "MAIL-007",
        "MAIL-008",
        "MAIL-009",
        "MAIL-010",
        "MAIL-011",
        "MAIL-012",
        "MAIL-013",
        "MAIL-014",
        "CHAN-001",
        "CHAN-002",
        "CHAN-003",
        "CHAN-004",
        "CHAN-005",
        "CHAN-006"
      ]
    },
    {
      "id": "customers-and-record-links",
      "description": "Customer and organisation records, history, imports, suspension, merge, and honest optional record links.",
      "clauses": [
        "CUST-001",
        "CUST-002",
        "CUST-003",
        "CUST-004",
        "CUST-005",
        "CUST-006",
        "CUST-007",
        "CUSTOMER-001",
        "CUSTOMER-002"
      ]
    },
    {
      "id": "help-centre",
      "description": "A public Help Centre with article lifecycle, taxonomy, search, translations, audience preview, and request forms.",
      "clauses": [
        "HELP-001",
        "HELP-002",
        "HELP-003",
        "HELP-004",
        "HELP-005"
      ]
    },
    {
      "id": "operations",
      "description": "Administration, macros, business-hour service targets, exact queues, and deterministic ticket automation.",
      "clauses": [
        "ADMIN-001",
        "ADMIN-002",
        "MACRO-001",
        "MACRO-002",
        "MACRO-003",
        "SLA-001",
        "SLA-002",
        "SLA-003",
        "SLA-004",
        "SLA-005",
        "SLA-006",
        "QUEUE-001",
        "QUEUE-002",
        "QUEUE-003",
        "QUEUE-004",
        "QUEUE-005",
        "AUTO-001",
        "AUTO-002",
        "AUTO-003",
        "AUTO-004",
        "AUTO-005",
        "AUTO-006",
        "AUTO-007"
      ]
    },
    {
      "id": "notifications-and-insight",
      "description": "Ticket notifications, satisfaction surveys, dashboards, report builder, filtered analytics, and export.",
      "clauses": [
        "NOTIFY-001",
        "NOTIFY-002",
        "NOTIFY-003",
        "NOTIFY-004",
        "CSAT-001",
        "CSAT-002",
        "CSAT-003",
        "INSIGHT-001",
        "INSIGHT-002",
        "INSIGHT-003",
        "INSIGHT-004",
        "INSIGHT-005",
        "INSIGHT-006",
        "INSIGHT-007",
        "INSIGHT-008"
      ]
    },
    {
      "id": "portable-data-and-migration",
      "description": "Portable owned data, clear adapter boundaries, and an idempotent Zendesk-export importer.",
      "clauses": [
        "DATA-001",
        "DATA-002",
        "DATA-003"
      ]
    }
  ],
  "nonGoals": [
    "Multiple businesses or tenants",
    "A separately browsable conversation inventory",
    "An owned CRM, commerce, order, payment, fulfilment, or marketing system",
    "Zendesk community, badges, moderation, theme marketplace, dashboard sharing or restrictions, CRM suite/tasks/prospecting, native mobile SDK, or social connectors",
    "Application marketplace, classic report builder, vendor benchmark survey, product switcher, signup/trial/billing chrome, or workforce management",
    "Outbound campaigns, arbitrary external webhooks, native mobile applications, or voice recording",
    "Autonomous customer messages, ticket lifecycle changes, external mutations, or fabricated adapter data",
    "Freshdesk replacement or import until compliant mapped public-source evidence exists"
  ],
  "externals": [
    {
      "id": "native-auth-mail",
      "required": true,
      "why": "Cloudflare primitives do not originate transactional email; Lily uses the explicitly selected Resend or SendGrid transport to deliver native magic-link sign-in messages.",
      "data": [
        "allow-listed operator email address",
        "signed magic-link URL",
        "delivery metadata required to send the sign-in message"
      ],
      "adapters": []
    },
    {
      "id": "mail-provider",
      "required": false,
      "requiredWhen": "A deployment enables inbound or outbound email.",
      "why": "A configured provider supplies inbound delivery callbacks, outbound email transport, and authoritative delivery reports.",
      "data": [
        "customer email addresses",
        "message content",
        "email attachments",
        "threading metadata",
        "delivery event metadata"
      ],
      "adapters": [
        "mail.receiver.v1",
        "mail.sender.v1"
      ]
    },
    {
      "id": "record-link-provider",
      "required": false,
      "requiredWhen": "A deployment enables optional external record links.",
      "why": "A buyer-controlled adapter may expose named external records without making Lily an owned CRM or commerce system.",
      "data": [
        "buyer-selected external record identifiers",
        "adapter retrieval timestamps",
        "approved display fields"
      ],
      "adapters": [
        "record.link.v1"
      ]
    },
    {
      "id": "commerce-and-ai-provider",
      "required": false,
      "requiredWhen": "A buyer elects to implement the separately specified optional commerce adapter.",
      "why": "The retained optional specification requires replaceable commerce and AI providers while keeping manual core ticket work independent.",
      "data": [
        "buyer-approved ticket evidence",
        "buyer-approved external resource identifiers",
        "configured resolution instructions"
      ],
      "adapters": [
        "commerce.platform.v1",
        "ai.provider.v1"
      ]
    },
    {
      "id": "help-centre-ai-provider",
      "required": false,
      "requiredWhen": "A deployment enables the generative Help Centre bot.",
      "why": "Article-grounded generated visitor answers require a buyer-selected model provider.",
      "data": [
        "approved Help Centre article material",
        "disclosed visitor input",
        "validated article citation identifiers"
      ],
      "adapters": [
        "ai.provider.v1"
      ]
    },
    {
      "id": "voice-provider",
      "required": false,
      "requiredWhen": "A deployment enables the optional voice queue and call-ticket channel.",
      "why": "A buyer-selected telephony service supplies phone lines, queue state, and call records that Cloudflare primitives do not originate.",
      "data": [
        "caller and destination numbers",
        "call state and timing",
        "buyer-selected queue or line identifiers",
        "selected ticket identifier"
      ],
      "adapters": [
        "voice.provider.v1"
      ]
    },
    {
      "id": "external-sync-provider",
      "required": false,
      "requiredWhen": "A buyer installs the optional external.sync.v1 extension.",
      "why": "A buyer-controlled provider transport is required only when that extension synchronises records outside Lily.",
      "data": [
        "normalised provider namespace",
        "external resource identifier",
        "keyed erasure-suppression digest"
      ],
      "adapters": [
        "external.sync.v1"
      ]
    },
    {
      "id": "sendgrid",
      "why": "Optional explicitly selected provider for native sign-in, signed inbound mail, human-reviewed public replies, and signed delivery events. Provider acceptance is not delivery confirmation.",
      "adapters": [
        "mail.sender.v1",
        "mail.receiver.v1"
      ],
      "required": false,
      "requiredWhen": "MAIL_PROVIDER or TICKET_MAIL_PROVIDER is sendgrid.",
      "data": [
        "Reviewed message body",
        "Explicit recipients",
        "Attachments",
        "Provider acceptance reference"
      ]
    }
  ],
  "env": [
    {
      "name": "APP_ORIGIN",
      "required": true,
      "secret": false,
      "why": "Canonical HTTPS origin for links and origin checks."
    },
    {
      "name": "INITIAL_OWNER_EMAIL",
      "required": true,
      "secret": false,
      "why": "Initial allow-listed Administrator created during setup."
    },
    {
      "name": "SESSION_SECRET",
      "required": true,
      "secret": true,
      "why": "Native session signing."
    },
    {
      "name": "SUPPORT_EMAIL",
      "required": true,
      "secret": false,
      "why": "Required transactional sender identity for native magic-link sign-in mail; ticket email channels remain separately optional."
    },
    {
      "name": "NOREPLY_EMAIL",
      "required": false,
      "secret": false,
      "requiredWhen": "A deployment enables email notifications or satisfaction requests.",
      "why": "No-reply sender for non-conversation mail."
    },
    {
      "name": "MAIL_API_KEY",
      "required": true,
      "secret": true,
      "why": "API credential for the explicitly selected mail provider; never used with an unselected provider."
    },
    {
      "name": "TICKET_MAIL_API_KEY",
      "required": false,
      "secret": true,
      "requiredWhen": "A deployment enables inbound or outbound ticket email.",
      "why": "API credential for the explicitly selected mail provider; never used with an unselected provider."
    },
    {
      "name": "TICKET_MAIL_DEFAULT_PENDING_REASON_ID",
      "required": false,
      "secret": false,
      "requiredWhen": "A deployment enables inbound or outbound ticket email.",
      "why": "Configured customer-waiting pending reason used after a public ticket-mail reply."
    },
    {
      "name": "TICKET_MAIL_REPLY_TOKEN_SECRET",
      "required": false,
      "secret": true,
      "requiredWhen": "A deployment enables inbound or outbound ticket email.",
      "why": "Opaque ticket reply-token encryption key for the optional ticket-mail channel."
    },
    {
      "name": "TICKET_MAIL_WEBHOOK_SECRET",
      "required": false,
      "secret": true,
      "requiredWhen": "TICKET_MAIL_PROVIDER is resend and the ticket channel is configured.",
      "why": "Svix callback verification credential for the optional ticket-mail channel."
    },
    {
      "name": "COMMERCE_API_KEY",
      "required": false,
      "secret": true,
      "requiredWhen": "A buyer enables the optional commerce adapter.",
      "why": "Buyer-controlled optional commerce adapter credential."
    },
    {
      "name": "COMMERCE_WEBHOOK_SECRET",
      "required": false,
      "secret": true,
      "requiredWhen": "A buyer enables the optional commerce adapter callbacks.",
      "why": "Buyer-controlled optional commerce callback verification credential."
    },
    {
      "name": "EXTERNAL_SYNC_ERASURE_HMAC_KEY",
      "required": false,
      "secret": true,
      "requiredWhen": "A buyer installs the optional external.sync.v1 extension.",
      "why": "Key for one-way external-sync erasure suppression digests."
    },
    {
      "name": "AI_API_KEY",
      "required": false,
      "secret": true,
      "requiredWhen": "A deployment enables the generative Help Centre bot or optional commerce adapter AI.",
      "why": "Buyer-controlled AI provider credential."
    },
    {
      "name": "MAIL_PROVIDER",
      "required": false,
      "secret": false,
      "why": "Explicit outbound authentication provider: resend (default) or sendgrid. MAIL_API_KEY belongs to the selected provider."
    },
    {
      "name": "TICKET_MAIL_PROVIDER",
      "required": false,
      "secret": false,
      "why": "Explicit ticket outbound provider: resend (default, with receiving webhooks) or sendgrid (public intake and outbound only). TICKET_MAIL_API_KEY belongs to the selected provider."
    },
    {
      "name": "SENDGRID_INBOUND_PUBLIC_KEY",
      "required": false,
      "secret": false,
      "why": "Base64 SPKI P-256 verification key from the dedicated SendGrid Inbound Parse security policy. Enables signed SendGrid intake."
    },
    {
      "name": "RER_MAIL_RELAY_SECRET",
      "required": false,
      "secret": true,
      "why": "Optional minimum32-character HMACsecret for the owner-operated SendGrid receipt relay."
    },
    {
      "name": "SENDGRID_EVENT_PUBLIC_KEY",
      "required": false,
      "secret": false,
      "why": "Optional base64 SPKI key for direct signed SendGrid Event Webhook reports."
    },
    {
      "name": "TWILIO_ACCOUNT_SID",
      "required": false,
      "secret": true,
      "why": "Optional Twilio voice read adapter account; configure together with TWILIO_AUTH_TOKEN and a queue in Channels."
    },
    {
      "name": "TWILIO_AUTH_TOKEN",
      "required": false,
      "secret": true,
      "why": "Optional Twilio voice read credential. No outbound calls are initiated by Lily."
    },
    {
      "name": "AI_PROVIDER",
      "required": false,
      "secret": false,
      "why": "Optional grounded widget bot provider: openai. Requires AI_MODEL and OPENAI_API_KEY."
    },
    {
      "name": "AI_MODEL",
      "required": false,
      "secret": false,
      "why": "Explicit OpenAI model selection for optional grounded bot (verified gpt-5.6-luna)."
    },
    {
      "name": "OPENAI_API_KEY",
      "required": false,
      "secret": true,
      "why": "Optional OpenAI credential; only approved public article material and disclosed visitor turns are sent."
    },
    {
      "name": "RECORD_LINK_ADAPTER_NAME",
      "required": false,
      "secret": false,
      "why": "Optional named external record adapter. Configure together with HTTPS base URL and API token."
    },
    {
      "name": "RECORD_LINK_BASE_URL",
      "required": false,
      "secret": false,
      "why": "Optional fixed HTTPS read-only record endpoint, configured with named adapter and token; redirects are rejected."
    },
    {
      "name": "RECORD_LINK_API_TOKEN",
      "required": false,
      "secret": true,
      "why": "Optional bearer credential for the named read-only record adapter. Never returned to the browser."
    }
  ],
  "deploy": {
    "healthPath": "/health",
    "operatorPath": "/app",
    "publicPaths": [
      "/",
      "/health",
      "/assets/",
      "/signin",
      "/auth/callback",
      "/auth/invite/callback",
      "/api/auth/local-development",
      "/api/auth/local-development/sign-in",
      "/api/auth/magic-link",
      "/help",
      "/help/",
      "/api/public/help/",
      "/api/public/widget/",
      "/_serverFn/",
      "/webhooks/mail",
      "/survey/",
      "/api/public/survey/",
      "/webhooks/sendgrid/inbound",
      "/webhooks/sendgrid/events"
    ],
    "notes": "The generated /_serverFn/ transport contains rate-limited public Help Centre reads and independently session-protected private functions; listing it makes the Worker reachability explicit and does not grant private function access."
  },
  "customFields": [
    "ticket",
    "customer",
    "organisation"
  ],
  "extensionPoints": {
    "policies": [
      "ticket.assignment.v1",
      "ticket.auto-close.v1",
      "ticket.reply-status.v1",
      "ticket.followers.v1",
      "requester.suspension.v1",
      "channel.autoresponder.v1",
      "service-target.selection.v1",
      "automation.action-allowlist.v1",
      "ticket.quiet-hours.v1",
      "csat.request.v1"
    ],
    "events": [
      "ticket.created.v1",
      "ticket.message.received.v1",
      "ticket.message.sent.v1",
      "ticket.assigned.v1",
      "ticket.status.changed.v1",
      "ticket.merged.v1",
      "customer.updated.v1",
      "automation.run.completed.v1",
      "service-target.changed.v1",
      "satisfaction.received.v1"
    ],
    "slots": [
      "app.navigation.after.v1",
      "home.dashboard.after.v1",
      "ticket.queue.row.actions.v1",
      "ticket.header.actions.after.v1",
      "ticket.timeline.after.v1",
      "ticket.composer.toolbar.after.v1",
      "ticket.requester-context.after.v1",
      "customer.profile.after.v1",
      "help.article.after.v1",
      "settings.after.v1"
    ],
    "adapters": [
      "mail.receiver.v1",
      "mail.sender.v1",
      "record.link.v1",
      "voice.provider.v1",
      "external.sync.v1",
      "commerce.platform.v1",
      "ai.provider.v1"
    ],
    "contributions": [
      "app.route.v1",
      "app.navigation.v1",
      "settings.section.v1",
      "job.consumer.v1",
      "cron.task.v1"
    ]
  },
  "limits": {
    "status": "estimated",
    "basis": "Capacity and rate values are not load tested and must be replaced by measured limits before the first release. Public request values are per-IP one-minute enforcement thresholds, not capacity proof. Attachment content types are a product decision, not a measurement.",
    "concurrentOperators": 25,
    "tickets": 50000,
    "messages": 250000,
    "inboundMessagesPerMinute": 20,
    "outboundMessagesPerMinute": 20,
    "activeAutomationRules": 20,
    "attachmentBytesPerFile": 10485760,
    "attachmentBytesTotal": 10737418240,
    "attachmentContentTypes": [
      "image/png",
      "image/jpeg",
      "image/gif",
      "image/webp",
      "application/pdf",
      "text/plain",
      "text/csv",
      "message/rfc822",
      "application/zip",
      "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
      "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    ],
    "publicRequestsPerMinutePerIp": {
      "assets": 240,
      "health": 60,
      "landing": 60,
      "signIn": 10,
      "survey": 30,
      "providerWebhooks": 120,
      "integrations": 120,
      "helpCentre": 60,
      "helpRequest": 20,
      "widget": 30
    }
  },
  "accessibility": {
    "level": "WCAG 2.2 AA",
    "status": "target; automated and manual checks pending the Stage 2 backend port"
  },
  "operatingCost": {
    "status": "free-plan default; production usage not measured",
    "estimate": "The locally deployable core uses Cloudflare Workers, D1, R2, Queues, and Cron. Mail, record-link, commerce, and AI provider costs exist only when a buyer enables those adapters and are paid directly by that buyer. Published cost and capacity claims await Stage 2 measurement.",
    "included": [
      "Cloudflare Worker",
      "D1",
      "R2",
      "Queues",
      "Cron"
    ],
    "excluded": [
      "mail provider",
      "record-link provider",
      "voice provider",
      "optional commerce provider",
      "optional AI provider"
    ]
  },
  "composition": {
    "base": "base",
    "family": "support",
    "edition": "lily",
    "modules": [
      "support-intake",
      "support-email",
      "sendgrid-email"
    ]
  }
}
