{
  "schemaVersion": "1",
  "source": {
    "title": "Team Invites",
    "path": "examples/team-invites.md",
    "sha256": "81b8bd29fa5b91401a2fe3cbb367516d9f09865f098b7d2cce7ab609d3d0a51f"
  },
  "generator": {
    "name": "heuristic"
  },
  "requirements": [
    {
      "id": "REQ-456a6d",
      "text": "Workspace admins can invite teammates by entering one or more email addresses.",
      "kind": "functional",
      "priority": "should",
      "details": [
        "When an admin submits up to 20 valid addresses, then each address receives an invite email within 2 minutes.",
        "If an address is malformed, then the form shows an inline error and no invite is sent for it.",
        "If the address already belongs to a member, then the admin sees \"already a member\"."
      ],
      "source": {
        "line": 27,
        "endLine": 30,
        "section": [
          "Team Invites",
          "Requirements",
          "Sending invites"
        ]
      }
    },
    {
      "id": "REQ-b5ddb7",
      "text": "Workspace admins can set the role (Admin or Member) for each invite.",
      "kind": "functional",
      "priority": "should",
      "details": [
        "Given a role is selected, when the invite is accepted, then the new member has that role."
      ],
      "source": {
        "line": 31,
        "endLine": 32,
        "section": [
          "Team Invites",
          "Requirements",
          "Sending invites"
        ]
      }
    },
    {
      "id": "REQ-cb792f",
      "text": "Workspace admins can resend a pending invite.",
      "kind": "functional",
      "priority": "should",
      "details": [
        "Resending generates a new link and invalidates the previous one."
      ],
      "source": {
        "line": 33,
        "endLine": 34,
        "section": [
          "Team Invites",
          "Requirements",
          "Sending invites"
        ]
      }
    },
    {
      "id": "REQ-c1b247",
      "text": "Invitees can accept an invite from the email link and join the workspace.",
      "kind": "functional",
      "priority": "should",
      "details": [
        "When an invitee with an existing account opens a valid link, then they join the workspace after one confirmation click.",
        "When an invitee without an account opens a valid link, then they are taken to sign-up with the email prefilled."
      ],
      "source": {
        "line": 38,
        "endLine": 40,
        "section": [
          "Team Invites",
          "Requirements",
          "Accepting invites"
        ]
      }
    },
    {
      "id": "REQ-2ab69f",
      "text": "Invite links must expire after 7 days.",
      "kind": "functional",
      "priority": "must",
      "details": [
        "If an invitee opens an expired link, then they see an expiry message and a button to request a new invite."
      ],
      "source": {
        "line": 41,
        "endLine": 42,
        "section": [
          "Team Invites",
          "Requirements",
          "Accepting invites"
        ]
      }
    },
    {
      "id": "REQ-b7f7fa",
      "text": "The invite flow should be fast and easy for invitees.",
      "kind": "functional",
      "priority": "should",
      "details": [],
      "source": {
        "line": 43,
        "endLine": 43,
        "section": [
          "Team Invites",
          "Requirements",
          "Accepting invites"
        ]
      }
    },
    {
      "id": "REQ-51ea02",
      "text": "Workspace admins can view all pending invites with sender, role and sent date.",
      "kind": "functional",
      "priority": "should",
      "details": [],
      "source": {
        "line": 47,
        "endLine": 47,
        "section": [
          "Team Invites",
          "Requirements",
          "Managing invites"
        ]
      }
    },
    {
      "id": "REQ-b39715",
      "text": "Workspace admins can revoke a pending invite.",
      "kind": "functional",
      "priority": "should",
      "details": [
        "When an admin revokes an invite, then its link stops working immediately."
      ],
      "source": {
        "line": 48,
        "endLine": 49,
        "section": [
          "Team Invites",
          "Requirements",
          "Managing invites"
        ]
      }
    },
    {
      "id": "REQ-55acad",
      "text": "The system must record an audit log entry for every invite sent, accepted, resent or revoked.",
      "kind": "functional",
      "priority": "must",
      "details": [
        "Each entry includes actor, target email, action and timestamp in UTC."
      ],
      "source": {
        "line": 50,
        "endLine": 51,
        "section": [
          "Team Invites",
          "Requirements",
          "Managing invites"
        ]
      }
    },
    {
      "id": "REQ-9b69c7",
      "text": "Invite emails must be sent through the existing transactional email provider.",
      "kind": "non-functional",
      "priority": "must",
      "details": [],
      "source": {
        "line": 55,
        "endLine": 55,
        "section": [
          "Team Invites",
          "Non-functional requirements"
        ]
      }
    },
    {
      "id": "REQ-03f995",
      "text": "The system must rate-limit invites to 200 per workspace per day.",
      "kind": "non-functional",
      "priority": "must",
      "details": [],
      "source": {
        "line": 56,
        "endLine": 56,
        "section": [
          "Team Invites",
          "Non-functional requirements"
        ]
      }
    },
    {
      "id": "REQ-c74591",
      "text": "Invite tokens must be single-use and at least 128 bits of entropy.",
      "kind": "non-functional",
      "priority": "must",
      "details": [],
      "source": {
        "line": 57,
        "endLine": 57,
        "section": [
          "Team Invites",
          "Non-functional requirements"
        ]
      }
    }
  ],
  "outOfScope": [
    {
      "text": "Inviting users via SCIM or directory sync.",
      "source": {
        "line": 61,
        "endLine": 61,
        "section": [
          "Team Invites",
          "Out of scope"
        ]
      }
    },
    {
      "text": "Bulk CSV upload of invitees.",
      "source": {
        "line": 62,
        "endLine": 62,
        "section": [
          "Team Invites",
          "Out of scope"
        ]
      }
    },
    {
      "text": "Custom invite email branding.",
      "source": {
        "line": 63,
        "endLine": 63,
        "section": [
          "Team Invites",
          "Out of scope"
        ]
      }
    }
  ],
  "epics": [
    {
      "id": "EP-2f525c",
      "title": "Sending invites",
      "description": "Requirements under \"Team Invites > Requirements > Sending invites\".",
      "requirementIds": [
        "REQ-456a6d",
        "REQ-b5ddb7",
        "REQ-cb792f"
      ]
    },
    {
      "id": "EP-8bb632",
      "title": "Accepting invites",
      "description": "Requirements under \"Team Invites > Requirements > Accepting invites\".",
      "requirementIds": [
        "REQ-c1b247",
        "REQ-2ab69f",
        "REQ-b7f7fa"
      ]
    },
    {
      "id": "EP-2f8b9d",
      "title": "Managing invites",
      "description": "Requirements under \"Team Invites > Requirements > Managing invites\".",
      "requirementIds": [
        "REQ-51ea02",
        "REQ-b39715",
        "REQ-55acad"
      ]
    },
    {
      "id": "EP-ea441d",
      "title": "Team Invites: non-functional requirements",
      "description": "Requirements under \"Team Invites > Non-functional requirements\".",
      "requirementIds": [
        "REQ-9b69c7",
        "REQ-03f995",
        "REQ-c74591"
      ]
    }
  ],
  "stories": [
    {
      "id": "ST-456a6d",
      "epicId": "EP-2f525c",
      "title": "Invite teammates by entering one or more email addresses",
      "asA": "workspace admin",
      "iWant": "to invite teammates by entering one or more email addresses",
      "soThat": "it supports the goal \"Let workspace owners add their team without contacting support\"",
      "acceptanceCriteria": [
        {
          "id": "AC-456a6d-1",
          "given": "a workspace admin",
          "when": "an admin submits up to 20 valid addresses",
          "then": "each address receives an invite email within 2 minutes",
          "origin": "prd"
        },
        {
          "id": "AC-456a6d-2",
          "given": "a workspace admin",
          "when": "an address is malformed",
          "then": "the form shows an inline error and no invite is sent for it",
          "origin": "prd"
        },
        {
          "id": "AC-456a6d-3",
          "given": "a workspace admin",
          "when": "the address already belongs to a member",
          "then": "the admin sees \"already a member\"",
          "origin": "prd"
        }
      ],
      "requirementIds": [
        "REQ-456a6d"
      ],
      "estimate": {
        "points": 5,
        "rationale": "base 1; +2 for 3 acceptance criteria; +1 measurable performance target (within 2 minutes) = score 4 -> 5 pts"
      },
      "priority": "should",
      "labels": [
        "benefit-inferred"
      ]
    },
    {
      "id": "ST-b5ddb7",
      "epicId": "EP-2f525c",
      "title": "Set the role (Admin or Member) for each invite",
      "asA": "workspace admin",
      "iWant": "to set the role (Admin or Member) for each invite",
      "soThat": "it supports the goal \"Keep workspace membership auditable for security reviews\"",
      "acceptanceCriteria": [
        {
          "id": "AC-b5ddb7-1",
          "given": "a role is selected",
          "when": "the invite is accepted",
          "then": "the new member has that role",
          "origin": "prd"
        }
      ],
      "requirementIds": [
        "REQ-b5ddb7"
      ],
      "estimate": {
        "points": 1,
        "rationale": "base 1 = score 1 -> 1 pt"
      },
      "priority": "should",
      "labels": [
        "benefit-inferred"
      ]
    },
    {
      "id": "ST-cb792f",
      "epicId": "EP-2f525c",
      "title": "Resend a pending invite",
      "asA": "workspace admin",
      "iWant": "to resend a pending invite",
      "soThat": "it supports the goal \"Keep workspace membership auditable for security reviews\"",
      "acceptanceCriteria": [
        {
          "id": "AC-cb792f-1",
          "given": "a workspace admin",
          "when": "the workspace admin resends a pending invite",
          "then": "resending generates a new link and invalidates the previous one",
          "origin": "prd"
        }
      ],
      "requirementIds": [
        "REQ-cb792f"
      ],
      "estimate": {
        "points": 1,
        "rationale": "base 1 = score 1 -> 1 pt"
      },
      "priority": "should",
      "labels": [
        "benefit-inferred"
      ]
    },
    {
      "id": "ST-c1b247",
      "epicId": "EP-8bb632",
      "title": "Accept an invite from the email link and join the workspace",
      "asA": "invitee",
      "iWant": "to accept an invite from the email link and join the workspace",
      "soThat": "it supports the goal \"Let workspace owners add their team without contacting support\"",
      "acceptanceCriteria": [
        {
          "id": "AC-c1b247-1",
          "given": "an invitee",
          "when": "an invitee with an existing account opens a valid link",
          "then": "they join the workspace after one confirmation click",
          "origin": "prd"
        },
        {
          "id": "AC-c1b247-2",
          "given": "an invitee",
          "when": "an invitee without an account opens a valid link",
          "then": "they are taken to sign-up with the email prefilled",
          "origin": "prd"
        }
      ],
      "requirementIds": [
        "REQ-c1b247"
      ],
      "estimate": {
        "points": 2,
        "rationale": "base 1; +1 for 2 acceptance criteria = score 2 -> 2 pts"
      },
      "priority": "should",
      "labels": [
        "benefit-inferred"
      ]
    },
    {
      "id": "ST-2ab69f",
      "epicId": "EP-8bb632",
      "title": "Invite links must expire after 7 days",
      "asA": "workspace admin",
      "iWant": "invite links to expire after 7 days",
      "soThat": "it supports the goal \"Let workspace owners add their team without contacting support\"",
      "acceptanceCriteria": [
        {
          "id": "AC-2ab69f-1",
          "given": "a workspace admin",
          "when": "an invitee opens an expired link",
          "then": "they see an expiry message and a button to request a new invite",
          "origin": "prd"
        }
      ],
      "requirementIds": [
        "REQ-2ab69f"
      ],
      "estimate": {
        "points": 1,
        "rationale": "base 1 = score 1 -> 1 pt"
      },
      "priority": "must",
      "labels": [
        "benefit-inferred"
      ]
    },
    {
      "id": "ST-b7f7fa",
      "epicId": "EP-8bb632",
      "title": "The invite flow should be fast and easy for invitees",
      "asA": "invitee",
      "iWant": "the invite flow to be fast and easy for invitees",
      "soThat": "it supports the goal \"Let workspace owners add their team without contacting support\"",
      "acceptanceCriteria": [
        {
          "id": "AC-b7f7fa-1",
          "given": "an invitee",
          "when": "the behaviour is exercised",
          "then": "the invite flow should be fast and easy for invitees",
          "origin": "inferred"
        }
      ],
      "requirementIds": [
        "REQ-b7f7fa"
      ],
      "estimate": {
        "points": 1,
        "rationale": "base 1 = score 1 -> 1 pt"
      },
      "priority": "should",
      "labels": [
        "benefit-inferred"
      ]
    },
    {
      "id": "ST-51ea02",
      "epicId": "EP-2f8b9d",
      "title": "View all pending invites with sender, role and sent date",
      "asA": "workspace admin",
      "iWant": "to view all pending invites with sender, role and sent date",
      "soThat": "it supports the goal \"Keep workspace membership auditable for security reviews\"",
      "acceptanceCriteria": [
        {
          "id": "AC-51ea02-1",
          "given": "a workspace admin",
          "when": "the workspace admin views all pending invites with sender, role and sent date",
          "then": "workspace admins can view all pending invites with sender, role and sent date",
          "origin": "inferred"
        }
      ],
      "requirementIds": [
        "REQ-51ea02"
      ],
      "estimate": {
        "points": 1,
        "rationale": "base 1 = score 1 -> 1 pt"
      },
      "priority": "should",
      "labels": [
        "benefit-inferred"
      ]
    },
    {
      "id": "ST-b39715",
      "epicId": "EP-2f8b9d",
      "title": "Revoke a pending invite",
      "asA": "workspace admin",
      "iWant": "to revoke a pending invite",
      "soThat": "it supports the goal \"Keep workspace membership auditable for security reviews\"",
      "acceptanceCriteria": [
        {
          "id": "AC-b39715-1",
          "given": "a workspace admin",
          "when": "an admin revokes an invite",
          "then": "its link stops working immediately",
          "origin": "prd"
        }
      ],
      "requirementIds": [
        "REQ-b39715"
      ],
      "estimate": {
        "points": 1,
        "rationale": "base 1 = score 1 -> 1 pt"
      },
      "priority": "should",
      "labels": [
        "benefit-inferred"
      ]
    },
    {
      "id": "ST-55acad",
      "epicId": "EP-2f8b9d",
      "title": "Record an audit log entry for every invite sent, accepted, resent or revoked",
      "asA": "workspace admin",
      "iWant": "the system to record an audit log entry for every invite sent, accepted, resent or revoked",
      "soThat": "it supports the goal \"Let workspace owners add their team without contacting support\"",
      "acceptanceCriteria": [
        {
          "id": "AC-55acad-1",
          "given": "a workspace admin",
          "when": "the system records an audit log entry for every invite sent, accepted, resent or revoked",
          "then": "each entry includes actor, target email, action and timestamp in UTC",
          "origin": "prd"
        }
      ],
      "requirementIds": [
        "REQ-55acad"
      ],
      "estimate": {
        "points": 2,
        "rationale": "base 1; +1 security or compliance (audit) = score 2 -> 2 pts"
      },
      "priority": "must",
      "labels": [
        "benefit-inferred"
      ]
    },
    {
      "id": "ST-9b69c7",
      "epicId": "EP-ea441d",
      "title": "Invite emails must be sent through the existing transactional email provider",
      "asA": "workspace admin",
      "iWant": "invite emails to be sent through the existing transactional email provider",
      "soThat": "it supports the goal \"Let workspace owners add their team without contacting support\"",
      "acceptanceCriteria": [
        {
          "id": "AC-9b69c7-1",
          "given": "a workspace admin",
          "when": "the behaviour is exercised",
          "then": "invite emails must be sent through the existing transactional email provider",
          "origin": "inferred"
        }
      ],
      "requirementIds": [
        "REQ-9b69c7"
      ],
      "estimate": {
        "points": 3,
        "rationale": "base 1; +2 external integration (email provider) = score 3 -> 3 pts"
      },
      "priority": "must",
      "labels": [
        "non-functional",
        "benefit-inferred"
      ]
    },
    {
      "id": "ST-03f995",
      "epicId": "EP-ea441d",
      "title": "Rate-limit invites to 200 per workspace per day",
      "asA": "workspace admin",
      "iWant": "the system to rate-limit invites to 200 per workspace per day",
      "soThat": "it supports the goal \"Reduce time from workspace creation to first collaborator joining to under 1 day\"",
      "acceptanceCriteria": [
        {
          "id": "AC-03f995-1",
          "given": "a workspace admin",
          "when": "the system rate-limits invites to 200 per workspace per day",
          "then": "the system must rate-limit invites to 200 per workspace per day",
          "origin": "inferred"
        }
      ],
      "requirementIds": [
        "REQ-03f995"
      ],
      "estimate": {
        "points": 3,
        "rationale": "base 1; +2 distributed-state complexity (rate-limit) = score 3 -> 3 pts"
      },
      "priority": "must",
      "labels": [
        "non-functional",
        "benefit-inferred"
      ]
    },
    {
      "id": "ST-c74591",
      "epicId": "EP-ea441d",
      "title": "Invite tokens must be single-use and at least 128 bits of entropy",
      "asA": "workspace admin",
      "iWant": "invite tokens to be single-use and at least 128 bits of entropy",
      "soThat": "it supports the goal \"Let workspace owners add their team without contacting support\"",
      "acceptanceCriteria": [
        {
          "id": "AC-c74591-1",
          "given": "a workspace admin",
          "when": "the behaviour is exercised",
          "then": "invite tokens must be single-use and at least 128 bits of entropy",
          "origin": "inferred"
        }
      ],
      "requirementIds": [
        "REQ-c74591"
      ],
      "estimate": {
        "points": 2,
        "rationale": "base 1; +1 security or compliance (tokens, entropy) = score 2 -> 2 pts"
      },
      "priority": "must",
      "labels": [
        "non-functional",
        "benefit-inferred"
      ]
    }
  ],
  "dependencies": [
    {
      "from": "ST-456a6d",
      "to": "ST-cb792f",
      "kind": "inferred",
      "reason": "acts on \"invit\", which ST-456a6d introduces"
    },
    {
      "from": "ST-456a6d",
      "to": "ST-c1b247",
      "kind": "inferred",
      "reason": "acts on \"invit\", which ST-456a6d introduces"
    },
    {
      "from": "ST-456a6d",
      "to": "ST-2ab69f",
      "kind": "inferred",
      "reason": "acts on \"invit\", which ST-456a6d introduces"
    },
    {
      "from": "ST-456a6d",
      "to": "ST-b7f7fa",
      "kind": "inferred",
      "reason": "acts on \"invit\", which ST-456a6d introduces"
    },
    {
      "from": "ST-456a6d",
      "to": "ST-51ea02",
      "kind": "inferred",
      "reason": "acts on \"invit\", which ST-456a6d introduces"
    },
    {
      "from": "ST-456a6d",
      "to": "ST-b39715",
      "kind": "inferred",
      "reason": "acts on \"invit\", which ST-456a6d introduces"
    },
    {
      "from": "ST-456a6d",
      "to": "ST-9b69c7",
      "kind": "inferred",
      "reason": "acts on \"invit\", which ST-456a6d introduces"
    },
    {
      "from": "ST-456a6d",
      "to": "ST-c74591",
      "kind": "inferred",
      "reason": "acts on \"invit\", which ST-456a6d introduces"
    }
  ],
  "risks": [
    {
      "id": "RISK-38ad73",
      "description": "Invite emails may land in spam folders, lowering acceptance rate.",
      "likelihood": "medium",
      "impact": "medium",
      "mitigation": "",
      "relatedIds": [
        "ST-c1b247",
        "ST-456a6d",
        "ST-9b69c7"
      ],
      "origin": "prd"
    },
    {
      "id": "RISK-e2b1e7",
      "description": "Rate limits that are too strict could block legitimate onboarding for large teams.",
      "likelihood": "medium",
      "impact": "medium",
      "mitigation": "",
      "relatedIds": [],
      "origin": "prd"
    },
    {
      "id": "RISK-ef2de2",
      "description": "Unresolved question: Should Members (not only Admins) be allowed to send invites?",
      "likelihood": "medium",
      "impact": "medium",
      "mitigation": "Resolve with the PRD owner before the related stories enter a sprint.",
      "relatedIds": [
        "ST-b5ddb7"
      ],
      "origin": "derived"
    }
  ]
}
